Custom list <string []> Sort
I have a list string[]
.
List<string[]> cardDataBase;
I need to sort this list by each string value of list item ( item[1]
) in custom order.
The custom ordering is a little tricky, ordering by those start symbols:
"MW1"
"FW"
"DN"
"MWSTX1CK"
"MWSTX2FF"
then order those letters with the following starting letters:
"A"
"Q"
"J"
"C"
"E"
"I"
"A"
and then the numbers indicated above.
pattern, left unordered list, right ordered:
MW1E10 MW1Q04
MWSTX2FFI06 MW1Q05
FWQ02 MW1E10
MW1Q04 MW1I06
MW1Q05 FWQ02
FWI01 FWI01
MWSTX2FFA01 DNC03
DNC03 MWSTX1CKC02
MWSTX1CKC02 MWSTX2FFI03
MWSTX2FFI03 MWSTX2FFI06
MW1I06 MWSTX2FFA01
I tried Linq, but right now I'm not very good at it and can't solve it on my own. Do I need a dictionary, regex or regex dictionary? What would be the best approach?
source to share
I think you are approaching this the wrong way. You are not sorting strings , you are sorting structured objects that are mangled as strings (someone aptly called this antipattern "stringly typed" ). Your requirements show that you know this structure, but it is not represented in the datastructure List<string[]>
, and it complicates your life. You have to parse this structure into a real type (struct or class) and then sort it.
enum PrefixCode { MW1, FW, DN, MWSTX1CK, MWSTX2FF, }
enum TheseLetters { Q, J, C, E, I, A, }
struct CardRecord : IComparable<CardRecord> {
public readonly PrefixCode Code;
public readonly TheseLetters Letter;
public readonly uint Number;
public CardRecord(string input) {
Code = ParseEnum<PrefixCode>(ref input);
Letter = ParseEnum<TheseLetters>(ref input);
Number = uint.Parse(input);
}
static T ParseEnum<T>(ref string input) { //assumes non-overlapping prefixes
foreach(T val in Enum.GetValues(typeof(T))) {
if(input.StartsWith(val.ToString())) {
input = input.Substring(val.ToString().Length);
return val;
}
}
throw new InvalidOperationException("Failed to parse: "+input);
}
public int CompareTo(CardRecord other) {
var codeCmp = Code.CompareTo(other.Code);
if (codeCmp!=0) return codeCmp;
var letterCmp = Letter.CompareTo(other.Letter);
if (letterCmp!=0) return letterCmp;
return Number.CompareTo(other.Number);
}
public override string ToString() {
return Code.ToString() + Letter + Number.ToString("00");
}
}
A program using the above procedure to process your example might be:
static class Program {
static void Main() {
var inputStrings = new []{ "MW1E10", "MWSTX2FFI06", "FWQ02", "MW1Q04", "MW1Q05",
"FWI01", "MWSTX2FFA01", "DNC03", "MWSTX1CKC02", "MWSTX2FFI03", "MW1I06" };
var outputStrings = inputStrings
.Select(s => new CardRecord(s))
.OrderBy(c => c)
.Select(c => c.ToString());
Console.WriteLine(string.Join("\n", outputStrings));
}
}
This creates the same ordering as in your example. In the real code, I would recommend you to specify types in accordance with what they represent rather than, for example TheseLetters
.
This solution - with a real parsing step - is excellent because you will almost certainly want to do more with this data at some point, and it will allow you to actually access the data components easily. In addition, it is understandable for a future maintainer as the reason for the order is somewhat clear. In contrast, if you decide to do complex string processing, it is often very difficult to understand what is going on (especially if it is part of a larger program, rather than a tiny example like here).
Creating new types is cheap. If the return value of a method doesn't quite "fit" in an existing type, just create a new one, even if it means 1000 types.
source to share
A bit false feeding, but I found this question quite interesting and maybe helpful to others, also added some comments to explain:
void Main()
{
var cardDatabase = new List<string>{
"MW1E10",
"MWSTX2FFI06",
"FWQ02",
"MW1Q04",
"MW1Q05",
"FWI01",
"MWSTX2FFA01",
"DNC03",
"MWSTX1CKC02",
"MWSTX2FFI03",
"MW1I06",
};
var orderTable = new List<string>[]{
new List<string>
{
"MW1",
"FW",
"DN",
"MWSTX1CK",
"MWSTX2FF"
},
new List<string>
{
"Q",
"J",
"C",
"E",
"I",
"A"
}
};
var test = cardDatabase.Select(input => {
var r = Regex.Match(input, "^(MW1|FW|DN|MWSTX1CK|MWSTX2FF)(A|Q|J|C|E|I|A)([0-9]+)$");
if(!r.Success) throw new Exception("Invalid data!");
// for each input string,
// we are going to split it into "substrings",
// eg: MWSTX1CKC02 will be
// [MWSTX1CK, C, 02]
// after that, we use IndexOf on each component
// to calculate "real" order,
// note that thirdComponent(aka number component)
// does not need IndexOf because it is already representing the real order,
// we still want to convert string to integer though, because we don't like
// "string ordering" for numbers.
return new
{
input = input,
firstComponent = orderTable[0].IndexOf(r.Groups[1].Value),
secondComponent = orderTable[1].IndexOf(r.Groups[2].Value),
thirdComponent = int.Parse(r.Groups[3].Value)
};
// and after it done,
// we start using LINQ OrderBy and ThenBy functions
// to have our custom sorting.
})
.OrderBy(calculatedInput => calculatedInput.firstComponent)
.ThenBy(calculatedInput => calculatedInput.secondComponent)
.ThenBy(calculatedInput => calculatedInput.thirdComponent)
.Select(calculatedInput => calculatedInput.input)
.ToList();
Console.WriteLine(test);
}
source to share
You can use Array.Sort () method . Where your first parameter is the [] string you are sorting, and the second parameter contains the complex logic for determining the order.
source to share