简体   繁体   中英

C# Splitting a List<string> Value

I have a List with values {"1 120 12", "1 130 22", "2 110 21", "2 100 18"}, etc.

List<string> myList = new List<string>();
myList.Add("1 120 12"); 
myList.Add("1 130 22"); 
myList.Add("2 110 21"); 
myList.Add("2 100 18");

I need to count based on the first number (ID) is and sum the consequent values for this IDs ie for ID = 1 -> 120+130=150 and 12+22=34 and so on... I have to return an array with these values.

I know I can get these individual values, add them to an array and split it by the empty space between them with something like:

string[] arr2 = arr[i].Split(' ');

and loop thru them to do the sum of each value, but... is there an easy way to do it straight using Lists or Linq Lambda expression?

You can do it in LINQ like this:

var result = myList.Select(x => x.Split(' ').Select(int.Parse))
                   .GroupBy(x => x.First())
                   .Select(x => x.Select(y => y.Skip(1).ToArray())
                                 .Aggregate(new [] {0,0}, (y,z) => new int[] {y[0] + z[0], y[1] + z[1]}));

First, the strings are split and converted to int, then they are grouped by ID, then the ID is dropped, and in the end, they are summed together.

But I strongly recommend not doing it in LINQ, because this expression is not easy to understand. If you do it the classic way with a loop, it is quite clear what is going on at first sight. But put this code containing the loop into a separate method, because that way it won't distract you and you still only call a one-liner as in the LINQ solution.

To do it straight, no LINQ, perhaps:

var d = new Dictionary<string, (int A, int B)>();

foreach(var s in myList){
  var bits = s.Split();
  if(!d.ContainsKey(bits[0])) 
    d[bits[0]] = (int.Parse(bits[1]), int.Parse(bits[2]));
  else { 
    (int A, int B) x = d[bits[0]];
    d[bits[0]] = (x.A + int.Parse(bits[1]), x.B + int.Parse(bits[2]));
  }
}

Using LINQ to parse the int, and switching to using TryGetValue, will tidy it up a bit:

var d = new Dictionary<int, (int A, int B)>();

foreach(var s in myList){
  var bits = s.Split().Select(int.Parse).ToArray();
  if(d.TryGetValue(bits[0], out (int A, int B) x)) 
    d[bits[0]] = ((x.A + bits[1], x.B + bits[2]));
  else 
    d[bits[0]] = (bits[1], bits[2]);
 
}

Introducing a local function to safely get either the existing nums in the dictionary or a (0,0) pair might reduce it a bit too:

var d = new Dictionary<int, (int A, int B)>();
(int A, int B) safeGet(int i) => d.ContainsKey(i) ? d[i]: (0,0);

foreach(var s in myList){
  var bits = s.Split().Select(int.Parse).ToArray();
  var nums = safeGet(bits[0]);
  d[bits[0]] = (bits[1] + nums.A, bits[2] + nums.B);
}

Is it any more readable than a linq version? Hmm... Depends on your experience with Linq, and tuples, I suppose..

I know this question already has a lot of answers, but I have not seen one yet that focuses on readability .

If you split your code into a parsing phase and a calculation phase , we can use LINQ without sacrificing readability or maintainability, because each phase only does one thing:

List<string> myList = new List<string>();
myList.Add("1 120 12"); 
myList.Add("1 130 22"); 
myList.Add("2 110 21"); 
myList.Add("2 100 18");

var parsed = (from item in myList
              let split = item.Split(' ')
              select new 
              { 
                  ID = int.Parse(split[0]),
                  Foo = int.Parse(split[1]),
                  Bar = int.Parse(split[2])
              });

var summed = (from item in parsed
              group item by item.ID into groupedByID
              select new 
              {
                  ID = groupedByID.Key,
                  SumOfFoo = groupedByID.Sum(g => g.Foo),
                  SumOfBar = groupedByID.Sum(g => g.Bar)
              }).ToList();

foreach (var s in summed)
{
    Console.WriteLine($"ID: {s.ID}, SumOfFoo: {s.SumOfFoo}, SumOfBar: {s.SumOfBar}");
}

fiddle

If you want, but I think it will be much easier to edit and optimize using the usual value. I don't find using this kind of logic inside LINQ will stay that way for a long period of time. Usually, we need to add more values, more parsing, etc. Make it not really suitable for everyday use.

    var query = myList.Select(a => a.Split(' ').Select(int.Parse).ToArray())
        .GroupBy(
          index => index[0], 
          amount => new
                {
                    First = amount[1],
                    Second = amount[2]
                }, 
          (index, amount) => new
                {
                    Index = index, 
                    SumFirst = amount.Sum(a => a.First), 
                    SumSecond = amount.Sum(a => a.Second) 
                }
                );

fiddle

is there an easy way to do it straight using Lists or Linq Lambda expression?

Maybe, is it wise to do this? Probably not. Your code will be hard to understand, impossible to unit test, the code will probably not be reusable, and small changes are difficult.

But let's first answer your question as a one LINQ statement:

const char separatorChar = ' ';
IEnumerable<string> inputText = ...
var result = inputtext.Split(separatorChar)
   .Select(text => Int32.Parse(text))
   .Select(numbers => new
     {
         Id = numbers.First()
         Sum = numbers.Skip(1).Sum(),
     }); 

Not reusable, hard to unit test, difficult to change, not efficient, do you need more arguments?

It would be better to have a procedure that converts one input string into a proper object that contains what your input string really represents.

Alas, you didn't tell us if every input string contains three integer numbers, of that some might contain invalid text, and some might contain more or less than three integer numbers.

You forgot to tell use what your input string represents. So I'll just make up an identifier:

class ProductSize
{
    public int ProductId {get; set;}     // The first number in the string
    public int Width {get; set;}         // The 2nd number
    public int Height {get; set;}        // The 3rd number
}

You need a static procedure with input a string, and output one ProductSize:

public static ProductSize FromText(string productSizeText)
{
    // Todo: check input
    const char separatorChar = ' ';
    var splitNumbers = productSizeText.Split(separatorChar)
        .Select(splitText => Int32.Parse(splitText))
        .ToList();

    return new ProductSize
    {
         ProductId = splitNumbers[0],
         Width = splitNumbers[1],
         Height = splitNumbers[2],
    };
}

I need to count based on the first number (ID) is and sum the consequent values for this IDs

After creating method ParseProductSize this is easy:

IEnumerable<string> textProductSizes = ...

var result = textProductSizes.Select(text => ProductSize.FromText(text))
   .Select(productSize => new
     {
         Id = productSize.Id,
         Sum = productSize.Width + productSize.Height,
     });

If your strings do not always have three numbers

If you don't have always three numbers, then you won't have Width and Height, but a property:

IEnumerable<int> Numbers {get; set;}        // TODO: invent proper name

And in ParseProductSize:

var splitText = productSizeText.Split(separatorChar);
        
return new ProductSize
{
     ProductId = Int32.Parse(splitText[0]),
     Numbers = splitText.Skip(1)
         .Select(text => Int32.Parse(text));

I deliberately keep it an IEnumerable, so if you don't use all Numbers, you won't have parsed numbers for nothing.

The LINQ:

var result = textProductSizes.Select(text => ProductSize.FromText(text))
   .Select(productSize => new
     {
         Id = productSize.Id,
         Sum = productSize.Numbers.Sum(),
     });

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM