0

Good day,

In my sample scenario, I'm trying to fetching all users from my database and put then on my dto where it has lists of ordering by alphabet letters given the following: A-L, M-Z

Here's my sample code with common OrderBy by the user name:

var users = await users.Users().ToListAsync();

return users.Select(u=>new CategorizedByLetterUserDto{
    ...
}).OrderBy(u=>u.Name);

So my sample CategorizedByLetterUserDto looks like this.

public class CategorizedByLetterUserDto {
     public IEnumerable<AtoL> AtoL {get;set}    
     public IEnumerable<MtoZ> MtoZ {get;set;}
     ...
}

public class AtoL{
     public int Id {get;set;}
     public string Name {get;set;}
}

public class MtoZ{
     public int Id {get;set;}
     public string Name {get;set;}
}

so on and so forth...

So the result will be (array)

 {
     categorizedByLetterUserDto: {
         atoL: [
         {
           ...
         }
         ],
         mtoZ: [
         {
           ...
         }
         ]
     }
 }
AppleCiderYummy
  • 369
  • 1
  • 7
  • 20

3 Answers3

2

Use GroupBy, something like this:

 var grouped = users.GroupBy(u => u.Name.CompareTo("M") < 0).OrderBy(g => g.Key).ToArray();
 return new CategorizedByLetterUserDto
 {
    AtoL = grouped[1].Select(x => new UserDto { Id = x.Id, Name = x.Name }),
    MtoZ = grouped[0].Select(x => new UserDto { Id = x.Id, Name = x.Name }),
 };

And don't create identical classes, use:

public class UserDto
{
    public int Id {get;set;}
    public string Name {get;set;}
}

You could use ToLookup instead and in this case it would be equivalent but see https://stackoverflow.com/a/10215531/224370 for details as to when it's not.

And if you wanted to split on more than one split point you could do something like:

string splits = "EMT";
var grouped = users.GroupBy(u => splits.Count(c => u.Name[0] > c))
                     .OrderBy(g => g.Key).ToArray(); ...

Note: Purists would prefer a sorted list of split characters and a binary search method to find the index, but for most practical smaller sets of split characters this will be faster and it's less code and it doesn't care what order the splits points are specified in.

Ian Mercer
  • 38,490
  • 8
  • 97
  • 133
  • I'm facing a red line error at `<` it says, cannot apply operator `<` of type of string and string – AppleCiderYummy May 15 '19 at 03:04
  • also, if it `CategorizedByLetterUserDto` consumed only by JS, you can reed off DTO class at all. Just switch `IEnumerable` to `IEnumerable` and `x => new UserDto { Id = x.Id, Name = x.Name }` to `x => new { Id = x.Id, Name = x.Name }` – vasily.sib May 15 '19 at 03:10
  • @vasily.sib .ToUpper() is not present after [0]. Only `.ToString(), .Equals` etc. – AppleCiderYummy May 15 '19 at 03:12
  • 1
    Sorry, use `u.Name.CompareTo("M") < 0` instead. @AppleCiderYummy String is IComparable but doesn't implement the < and > operators as sort orders may depend on Culture. – Ian Mercer May 15 '19 at 03:18
  • Is it guaranteed that groupby maintains the order of elements input? – Caius Jard May 15 '19 at 04:05
  • @CaiusJard Yes, for linq to objects but not everywhere, but assumed adding a Sort isn't really what the Op is stuck on here. ( see https://stackoverflow.com/a/1452511/224370 ) – Ian Mercer May 15 '19 at 04:08
  • Thank you sir for this info. I have one more question, what if I want to separate then into 4? a to f, g to l, m to t and u to z? what does `< 0` mean in the code @IanMercer? – AppleCiderYummy May 15 '19 at 05:18
  • @AppleCiderYummy `IComparable.CompareTo` returns -1,0, or 1 for <, ==, or > respectively. If you wanted to do more splits you could maybe use ternary expressions instead, something equivalent to `x < 'F' ? 1 : x < 'M' ? 2 : x < 'T' : 3 : 4`. I wouldn't do ASCII number math though (as suggested below) and I'd try to make it work for accented characters too. – Ian Mercer May 15 '19 at 06:59
  • @AppleCiderYummy see updated answer for more than one split point. – Ian Mercer May 15 '19 at 07:15
  • I understand sir. Because I tried the first one you wrote with a `CompareTo` `GroupBy` it only appears 2 separations. either true or false. But thank you for the updated answer. I also notifice using `EMT` `[0]` represents the T - Z `[1]` represents the M - S whle the `[2]` represents the A to E. Am I getting it correctly sir? @IanMercer – AppleCiderYummy May 15 '19 at 07:35
  • But what if there is no `[2]` or `[3]` sir? – AppleCiderYummy May 15 '19 at 08:21
  • Yes, if you want to do more splits you'll also need to decide how to handle those splits. Ideally you don't have separate properties for each group, instead return an array of objects, each of which has some identifier and then a list of the objects in it. `{ "A-M" : [ { Id:1, Name:"A"}, .... ], "N-Z" : ... }` – Ian Mercer May 15 '19 at 16:15
0

Better use the List<> Class instead of IEnumerable<> in your CategorizedByLetterUserDto class to access the .Add method.. Then try the ff code.

var users = await users.Users().ToListAsync();
char[] listOfAtoL = "ABCDEFGHIJKL".toCharArray();
CategorizedByLetterUserDto cat = new CategorizedByLetterUserDto();

foreach (User u in users.OrderBy(a => a.Name)) {
    listOfAtoL.Contains(char.ToUpper(u.Name.toCharArray()[0])) ? cat.AtoL.Add(new AtoL() {id = u.ID, Name = u.Name}) : cat.MtoZ.Add(new MtoZ() {id = u.ID, Name = u.Name});
}

return cat;
  • @MikeWodarczyk sorry, I'm not attention to detail. I've updated my answer.. – Felix Jacob Borlongan May 15 '19 at 02:53
  • `listOfMtoZ` is never used, also it is much more easier to check `u.Name.toCharArray()[0].toLowerCase() < 'M'` then `listOfAtoL.Contains(u.Name.toCharArray()[0].toLowerCase())` – vasily.sib May 15 '19 at 02:58
  • Aside from the Java style method naming, you made a gross coding error in your comment @vasily.sib - you ToLowerCase()d and then compared with uppercase M, which is always false. You also don't need to call tochararray either - strings can be addressed as if they were an array using simple [ ]. Seems you're running around downvoting things but giving bad advice with poor attention to detail.. – Caius Jard May 15 '19 at 03:49
  • @CaiusJard take it easy man, it's not my code. I just blindly copy-paste code from this answer to my comment. – vasily.sib May 15 '19 at 03:55
  • @CaiusJard and I'm not downvoting any of this 3 answers. Actually I upvoted the one by Ian Mercer – vasily.sib May 15 '19 at 03:57
  • @CaiusJard I can upvote for your answer as well, if this will console you – vasily.sib May 15 '19 at 04:00
  • @vasily, you're right, my apologies (though I would then say it should perhaps have been mentioned or fixed rather than copied blind ;) ) – Caius Jard May 15 '19 at 04:06
  • @felix please fix up your code- toLowerCase does not exist in C# and if you lowercase the name then compare it with M it will always be false. Also the OP's requirement that the output name lists be ordered seems to have been forgotten – Caius Jard May 15 '19 at 04:07
  • @CaiusJard Sorry, mistakenly used the extension of javascript instead of C#, and I already did used the ``OrderBy`` clause. – Felix Jacob Borlongan May 15 '19 at 04:27
0

I don't think you need to get that involved, you can just use LINQs existing functionality tocreate lookups

List<User> list = ... .OrderBy(u=>u.Name); //from db 

ILookup<int, User> lookup = list.ToLookup(
  u => (u.Name[0] - 63)/14,
  u => new Dto { Id = u.Id, Name = u.Name }
);

The numbers relate to treating a char as an int in the ascii table, A being 65. By subtracting 63, A becomes 2, L becomes 13, Z becomes 27. By then dividing by 14 we reduce this to 0 or 1. Your lookup will now be an enumerable of users indexed by either 0 or 1, the 0 being AtoL. If you want the users in a given list sorted you can call OrderBy on them. I did this with math rather than a bool compare so you can alter it to more columns in future just by altering the numbers, but you can use bool also:

var lookup = list.ToLookup(
  u => u.Name[0]<'M',
  u => new Dto { Id = u.Id, Name = u.Name }
);

In this the lookup returned, indexed by true would be A to L

Assign to your categorized Dto:

var x = new CatDto(){ 
  aToL=lookup[true],   
  mToZ=lookup[false] 
};

Note that this, in combination with every other answer here, relies on your names being properly formatted with an uppercase ASCII character as the first char.. If your names are messy, or don't always start thus, you should consider doing things like uppercasing them before compare etc

Caius Jard
  • 72,509
  • 5
  • 49
  • 80
  • Too much magic numbers here! `new User { Name = "aaa" }` will be in `lookup[2]` and `new User { Name = "zzz" }` will be in `lookup[4]`. Oh, and `new User { Name = "" }` will be in `lookup[3949]` – vasily.sib May 15 '19 at 03:28
  • The OP didn't say any of those would be in his names. The spec was A to L and M to Z - don't chastise someone for working to the spec given when you don't even know for certain that any of the names start with lowercase – Caius Jard May 15 '19 at 03:29
  • The OP also didn't say any of those wouldn't be – vasily.sib May 15 '19 at 03:30
  • And I'm sorry, but I'm not chastise anyone for working to the spec given. All I want to say is that checking letters with math may easily lead to unpredictable results. For example, if for some reason some user name will start from Unicode character, I think it is better that it will get in a results some how (will always get in `M to Z` list) then silently disapear in the middle of process. – vasily.sib May 15 '19 at 03:37
  • You cannot work on what an OP doesn't say a thing won't be, otherwise you'll end up coding for everything. The implication was simple- all names will start with A to Z. There are plenty of other methods here that will drop names outside this range and I don't see you picking on them. You're saying it's acceptable to add letter that are not in the range M to Z into that range? That could be just as bad as dropping them and I find your opinion of what is right/wrong to be harmful to the OP getting a good result – Caius Jard May 15 '19 at 03:39
  • (Would you really want a database to return you names starting with Unicode if you asked it for names starting with letters betweenness M and Z? I think not!!) – Caius Jard May 15 '19 at 03:45
  • 1
    I'm not offending you, it's OK with your answer and it will work greate until user names starts strictly with ASCII characters from `0x41` to `0x5A`. All I want to say is that when I read someone else code like this `list.ToLookup(u => u.Name.Compare("M"));` I think `ok, it comparing each user name with "M"`, but when I read `list.ToLookup(u => (u.Name[0] - 63)/14);` I think `wat? Nevermind, I hope this will just works`. – vasily.sib May 15 '19 at 03:51
  • It's a valid comment about it being magic numbers, and I've added some explanation for it. Thanks for the feedback – Caius Jard May 15 '19 at 03:57