1

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 i.e. 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?

2
  • why would you prefer Linq? I think it will be harder to read in Linq for your case. I think it should possible, but you will still need a temporary variable to store the result value. But, it will be harder to read and could be slower. need more tests though. Commented May 4, 2021 at 4:55
  • you should clarify whether the formatting of the string is consistent. If it is you can write an algorithm that will iterate through the list values. I don't think there is a more economic way to do this using linq Lamda Commented May 4, 2021 at 5:08

5 Answers 5

4

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.

Sign up to request clarification or add additional context in comments.

4 Comments

I think the Where should be a GroupBy so it's not just ID 1
@CaiusJard After reading the question again, I think you are right. I edited the question.
Lol.. I love the bit where you wrote "i edited the question" - I think you meant "answer", but having it as question really made me laugh
That's right! Of course I edited the answer :-)
2

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

Comments

2

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

Comments

1

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

Comments

1

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(),
     });

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.