1

I have an ArrayList that contains a large number of strings. It needs to be sorted in place based on three fields (essentially three substrings) which are Name, Age and Amt. Age is the first substring (position 0-3), Name is second (3-6) and Amt is last (6-10). The order in which these parameters are to be sorted is very important and is as follows:

First perform ascending sort by Name THEN do ascending sort by Age (which actually comes earlier in the substring) and THEN do descending sort by Amt. That's it.

I have this class

public class ArrComparer : IComparer
{
    public int Compare(object x, object y)
    {
        string left = x.ToString();
        string right = y.ToString();
        string lhs = left.Substring(3, 6);
        string rhs = right.Substring(3, 6);
        return lhs.CompareTo(rhs);
    }
}

which I use to sort based on just one field - Name by invoking

RecordList.Sort(new ArrComparer());

This lets me sort correctly based on that one field. The question is how can I modify this code to allow me to sort based on all three AT ONCE, in the right order and using proper asc/desc mode?

Any code or tips would be greatly appreciated. (By the way, in case you are wondering using generic List<T> is not an option in this project).

GonzoKnight
  • 809
  • 7
  • 12
  • 1
    Why is a generic list "not an option"? Why are you not using objects with three fields instead of strings? – Matti Virkkunen Mar 15 '11 at 19:47
  • Do you mind saying why generic lists are not an option? – John Saunders Mar 15 '11 at 19:48
  • 7
    Think it through. How do you do regular alphabetical sorting? You compare the first letters. If they're different, then you know the order. If they're equal, you compare the second letters. And so on. Same thing here. How do you sort by name, then age, then amount? Compare the two names. Are they unequal? Then you know the order. Are they equal? Then compare the ages. Are they unequal? Then you know the order. Are they equal? Then compare the amounts. – Eric Lippert Mar 15 '11 at 19:49
  • "Do you mind saying why generic lists are not an option?" This is a fair question and the simple answer is that this is a small part of a much bigger project that was written a while ago and relies on existing data structures. Changing them would be more difficult at this point then using what is there. – GonzoKnight Mar 15 '11 at 19:56
  • Eric, this is essentially a conversion from COBOL, this is how the results were sorted in the original program. Also, LINQ is an option as long as it could be used here. I actually know little about it at this point. Thanks to everyone who replied, by the way. – GonzoKnight Mar 15 '11 at 20:01

5 Answers5

6

ArrayList still implements IEnumerable, meaning you can use the simple orderby() and thenby() extensions in linq:

RecordList = new ArrayList(
         RecordList.Cast<string>().OrderBy(s => s.Substring(3,3))
                   .ThenBy(s => int.Parse(s.Substring(0,3)))
                   .ThenByDescending(s => double.Parse(s.Substring(6,4)))
          .ToArray());

Other ways to express this include building a more complicated .OrderBy() or using an anonymous type to compose your string as an object:

RecordList = new ArrayList(
       Record.Cast<string>().Select(s => new {source = s, age = int.Parse(s.Substring(0, 3)), name = s.Substring(3,3), amt = double.Parse(s.Substring(6,4))})
             .OrderBy(o => o.name)
             .ThenBy(o => o.age)
             .ThenByDescending(o => o.amt)
          .Select(o => o.source).ToArray());

I like that option because it sets you up to start thinking in terms objects. Play your cards right and you can skip that last .Select() projection to keep the objects rather than going back to strings, which will save the work of having to do all that parsing over again later.

If these aren't an option (possibly for the same reason you can't use List<T>), it's easy to modify your existing compare method like so:

public class ArrComparer : IComparer
{
    public int Compare(object x, object y)
    {
        int result;
        string left = x.ToString();
        string right = y.ToString();
        string lhs1 = left.Substring(3, 3);
        string rhs1 = right.Substring(3, 3);
        result = lhs1.CompareTo(rhs1);

        if (result == 0)
        {
           int lhs2 = int.Parse(left.Substring(0,3));
           int rhs2 = int.Parse(right.Substring(0,3));
           result = lhs2.CompareTo(rhs2);
        }

        if (result == 0)
        {
            double lhs3 = double.Parse(left.Substring(6,4));
            double rhs3 = double.Parse(right.Substring(6,4));
            result = rhs3.CompareTo(lhs3);
        }

        return result;
    }
}
Joel Coehoorn
  • 399,467
  • 113
  • 570
  • 794
  • 1
    this wouldn't do an *in place* sorting though as requested in OP's question – BrokenGlass Mar 15 '11 at 19:52
  • @Joel: you should use `Substring(3, 3)` instead of `Substring(3, 6)`, as the second argument is length. The same for `Substring(6, 10)`. – Vlad Mar 15 '11 at 20:02
  • @Vlad - of course you're right about that. In my defense, I was taking my queue from the code in the original question. – Joel Coehoorn Mar 15 '11 at 20:05
  • @Joel: besides that, it should be possible to omit `int.Parse` (at least inside `Compare(...)`), because the digits of age are left-padded to the same number of decimal places. Well, from the other side parsing illustrates the possibility of further transformations, so it's didactically justified. – Vlad Mar 15 '11 at 20:08
  • @Vlad - even if I didn't need it, I'd keep the int.Parse() in there unless it was shown to be driving a real performance hit or we started seeing mal-formated string(exceptions), as this helps future programmers know what is expected in that position of the string. – Joel Coehoorn Mar 15 '11 at 20:13
  • @Joel: the data structure is anyway inappropriate. A `struct` would do much better. (And it could implement `IComparable` as well.) – Vlad Mar 15 '11 at 20:14
  • @Vlad - The question does not make it clear but, actually, the data structure is very appropriate to its other uses, such as having the ability to insert new strings and being able to iterate through the entire object. – GonzoKnight Mar 15 '11 at 20:49
  • @GonzoKnight: I meant just replacing `string` by a special `struct`, for example `struct Entry { int age; string name; string amt; }` . You'll be able to insert strings or iterate through the whole list the same way. Moreover, I would try to replace `ArrayList` with a more type-safe `List`, if it's possible. Actually, you can see an anonymous structure like that in the Linq approach. – Vlad Mar 15 '11 at 20:57
  • @Vlad - thank you for the clarification - I will keep your suggestion in mind for future projects. – GonzoKnight Mar 15 '11 at 21:19
  • @Joel Coehoorn - I would be very curious to try out the above LINQ code samples you've written. However, both of them are giving them compilation errors saying something to the effect that the compiler cannot infer the type of the object and asks for explicit types. I tried changing s to string s but that only created more problems. Any ideas what else I can try. Once again, your response was very helpful. Thank you! – GonzoKnight Mar 15 '11 at 21:46
  • @Gonzo - I forgot the .ToString() calls in the 2nd example. That's what I get for typing directly into the reply window. Not sure just looking what's wrong with the first. What version of visual studio/.Net are you using? – Joel Coehoorn Mar 16 '11 at 01:35
  • Okay, okay took a few moments in visual studio to fix the samples. Should be all good now. – Joel Coehoorn Mar 16 '11 at 01:43
  • @Joel Coehoorn - Thank you very much for your responses. The LINQ queries are working now and were helpful in demonstrating how it can be used. – GonzoKnight Mar 16 '11 at 13:46
2

You can compare part by part:

string left = (string)x;
string right = (string)y;

string lname = left.Substring(3, 3);
string rname = right.Substring(3, 3);
int result = lname.CompareTo(rname);
if (result != 0) return result;

string lage = left.Substring(0, 3);
string rage = right.Substring(0, 3);
int result = lage.CompareTo(rage);
if (result != 0) return result;

string lamt = left.Substring(6);
string ramt = right.Substring(6);
return -lamt.CompareTo(ramt);
Vlad
  • 35,022
  • 6
  • 77
  • 199
1

If you need an IComparer, try something like:

public class ArrComparer : IComparer
{
  public int Compare(object x, object y)
  {
    string left = x.ToString();
    string right = y.ToString();
    string leftName = left.Substring([whatever]);
    string rightName = right.Substring([whatever]);

    // First try comparing names
    int result = leftName.CompareTo(rightName);
    if (result != 0)
    {
      return result;
    }

    // If that didn't work, compare ages
    string leftAge = left.Substring([whatever]);
    string rightAge = right.Substring([whatever]);
    result = leftAge.CompareTo(rightAge);
    if (result != 0)
    {
      return result;
    }    

    // Finally compare amounts (descending)
    string leftAmt = left.Substring([whatever]);
    string rightAmt = right.Substring([whatever]);
    result = -leftAmt.CompareTo(rightAmt); // Minus for descending

    return result;
  }
}
Jeremy Todd
  • 3,261
  • 1
  • 18
  • 17
1

I would recommend storing your records in an object, and make those comparable instead.

In order to compare all three fields using the same method you are currently using you simply need to extract all three pieces of data and do a full comparison.

public class ArrComparer : IComparer
{
    public int Compare(object x, object y)
    {
        string left = x.ToString();
        string right = y.ToString();

        // Note I assumed indexes since yours were overlapping.
        string lage = left.Substring(0, 3);
        string lname = left.Substring(3, 3);
        string lamt = left.Substring(7, 3);

        string rage = left.Substring(0, 3);
        string rname = left.Substring(3, 3);
        string ramt = left.Substring(7, 3);

        // Compare name first, if one is greater return
        int result = lname.CompareTo(rname);
        if (result != 0)
            return result;

        // else compare age, if one is greater return
        result = lage.CompareTo(rage)
        if (result != 0)
            return result;

        // else compare amt if one is greater return
        result = lamt.CompareTo(ramt)
        if (result != 0)
            return result;

        // else they are equal
        return 0;
    }
}
John Saunders
  • 160,644
  • 26
  • 247
  • 397
Cody
  • 3,734
  • 2
  • 24
  • 29
  • you should use `Substring(3, 3)` instead of `Substring(3, 6)`, as the second argument is length. The same for `Substring(7, 10)`. As well, `Substring(0, 2)` seems to be wrong. – Vlad Mar 15 '11 at 20:03
  • @Vlad Whoops! Thanks for the heads up. :) – Cody Mar 15 '11 at 20:05
  • Thanks for all of the responses, guys! I will need a bit of time to analyze them and try them out then I will comeback with feedback. – GonzoKnight Mar 15 '11 at 20:06
0

you could expend your ArrCompare with if statements like if(rhs == lhs) compere with other part of string. Accen deccend is meeter of return -1 or 1

kalvis
  • 560
  • 2
  • 10