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).

5
  • 1
    Why is a generic list "not an option"? Why are you not using objects with three fields instead of strings? Commented Mar 15, 2011 at 19:47
  • Do you mind saying why generic lists are not an option? Commented Mar 15, 2011 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. Commented Mar 15, 2011 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. Commented Mar 15, 2011 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. Commented Mar 15, 2011 at 20:01

5 Answers 5

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;
    }
}
Sign up to request clarification or add additional context in comments.

13 Comments

this wouldn't do an in place sorting though as requested in OP's question
@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 - of course you're right about that. In my defense, I was taking my queue from the code in the original question.
@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 - 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.
|
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);

Comments

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;
  }
}

Comments

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;
    }
}

3 Comments

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 Whoops! Thanks for the heads up. :)
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.
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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.