Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Sum a List, but only for values after a certain point

Tags:

c#

linq

I'm writing a web page to keep the score for a card game. Getting the players score so far would be easy, but there is a twist. During any round the players score can be reset to zero at the start of the round. I don't want to change the score for any previous rounds, so I only want to get a sum of the rounds after (and including) the reset. A player could potentially have their score reset multiple times in a game, or not at all.

I can get the correct score by a multiple stage process of finding the last (if any) score reset, and summing all hands after that (or all hands in no resets) - see PlayerGame.GetPlayerScore.

I'm still trying to get my head around the more intricate ways of doing things with LINQ, and I was wondering if there was a way to do this using a single LINQ statement?

Minimal code:

class Program
{
    static void Main(string[] args)
    {
        PlayerGame playerGame = new PlayerGame();

        playerGame.PlayerHands = new List<PlayerHand>
        {
            new PlayerHand { Round = 1, Score = 10 },
            new PlayerHand { Round = 2, Score = 20 },
            new PlayerHand { Round = 3, Score = 30 },
            new PlayerHand { Round = 4, Score = 40, Reset = true },
            new PlayerHand { Round = 5, Score = 50 },
            new PlayerHand { Round = 6, Score = 60 }
        };

        Console.WriteLine($"Players score was {playerGame.GetPlayerScore()}");
        Console.ReadLine();
    }
}

class PlayerHand
{
    public int Round { get; set; }
    public int Score { get; set; }
    public bool Reset { get; set; } = false;
}

class PlayerGame
{
    public List<PlayerHand> PlayerHands { get; set; }

    public PlayerGame()
    {
        PlayerHands = new List<PlayerHand> { };
    }

    public int GetPlayerScore()
    {
        // Can all this be simplified to a single LINQ statement?
        var ResetIndex = PlayerHands.OrderBy(t => t.Round).LastOrDefault(t => t.Reset == true);

        if (ResetIndex != null)
        {
            return PlayerHands.Where(t => t.Round >= ResetIndex.Round).Sum(t => t.Score);
        }
        else
        {
            return PlayerHands.Sum(t => t.Score);
        }
    }
}

https://dotnetfiddle.net/s5rSqJ

As presented, the players score should be 150. I.e. the score gets reset at the start of Round 4, so the total score is the sum of Rounds 4, 5, and 6.

like image 411
Slugsie Avatar asked May 24 '21 18:05

Slugsie


People also ask

How do you sum for a specific data range in Excel?

In a worksheet, tap the first empty cell after a range of cells that has numbers, or tap and drag to select the range of cells you want to calculate. Tap AutoSum. Tap Sum.

How do you sum a list in a range in Python?

The sum() function returns the sum of an iterable. Sum() takes a list (iterable) and returns the sum of the numbers within the list.


Video Answer


4 Answers

Summarizing a few points,

  • the number of rounds is finite (otherwise a really long game!). This observation is important when we talk about reversing below.
  • rounds are already sorted in ascending order (per the comments), so actual round number doesn't matter
  • if we sum backwards, we don't have to scan the whole list

So, we can come up with an implementation that is O(1) space (in-place, no allocations) and O(n) time (linear, less than the size of the list when there's a reset).

Using MoreLinq

var score = hands.ReverseInPlace().TakeUntil(x => x.Reset).Sum(x => x.Score);

Where ReverseInPlace() iterates in reverse order in place, and MoreEnumerable.TakeUntil() takes up to and including the round that has a true value for Reset or end of sequence.

ReverseInPlace would be an extension method (you could generalize to IEnumerable<> if you wanted).

public static class ListExtensions
{
    public static IEnumerable<T> ReverseInPlace<T>(this IList<T> source)
    {
        // add guard checks here, then do...
        for (int i=source.Length-1; i != -1; --i)
            yield return source[i];
    }
}

Not using MoreLinq

You could create a TakeInReverseUntil:

public static IEnumerable<T> TakeInReverseUntil<T>(this IList<T> source, Func<T, bool> predicate)
{
    // add guard checks here, then do...
    for (int i=source.Length-1; i != -1; --i)
    {
        yield return source[i];
        if (predicate(source[i]) yield break;
    }
}

giving you the simplified call

var score = hands.TakeInReverseUntil(x => x.Reset).Sum(x => x.Score);

NOTE: Enumerable.Reverse() allocates a buffer, so is O(n) space, and is why I rolled my own ReverseInPlace instead for this answer.

like image 130
Kit Avatar answered Oct 22 '22 16:10

Kit


The best way I can see here is to simply change your check for the last reset round a bit and combine both statements:

public int GetPlayerScore()
{
    // selects the highest Round if Reset == true or 1 by default
    var lastResetRound = PlayerHands.Max(hand => hand.Reset ? hand.Round : 1);

    return PlayerHands.Where(t => t.Round >= lastResetRound.Round).Sum(t => t.Score);
    
    // or all toghether like this:
    return PlayerHands.Where(t => t.Round >= PlayerHands.Max(hand => hand.Reset ? hand.Round : 1)).Sum(t => t.Score);
}
like image 35
Chrᴉz remembers Monica Avatar answered Oct 22 '22 18:10

Chrᴉz remembers Monica


If you used MoreLinq's TakeUntil() then you could do something like:

PlayerHands
    .OrderByDescending(x => x.Round)
    .TakeUntil(x => x.Reset)
    .Sum(x => x.Score);

Edit: formatting & simplified bool conditional per @PrasadTelkikar

like image 31
user2051770 Avatar answered Oct 22 '22 16:10

user2051770


I like the morelinq TakeUntil most which others have shown, missing it in the standard library.

I've tried to do it without creating new extension methods(cheating) and without morelinq. Following works, but is not as readable and also requires an ordered list(which is the case acc. to your comments).

return PlayerHands
    .TakeLast(PlayerHands.Count + 1 - (PlayerHands.FindLast(x => x.Reset)?.Round ?? 1))
    .Sum(x => x.Score);

Maybe someone finds a way to simplify the count-calculation.

like image 26
Tim Schmelter Avatar answered Oct 22 '22 16:10

Tim Schmelter