0

Based on a list of a typed class like

class CostCenter
{
    string center;
    string percentage;
    DateTime start;
}

I have a list of these elements like this:

  • {"a", 70, "2019/11/10"}
  • {"b", 30, "2019/11/10"}
  • {"A", 40, "2018/10/05"}
  • {"B", 60, "2018/10/05"}
  • {"xx", 10, "2010/01/01"}
  • {"yy", 90, "2010/01/01"}
  • {"100", 50, "2009/07/03"}
  • {"101", 50, "2009/07/03"}

taking into account that I want to get rid of all of the elements (list) that have a start date earlier or equal than a fixed(_earliestDate), also this fixed data will replace the earliest match. In this example the earliest date is "2010/11/01", therefore "xx" and "yy" start date is replaced to "2010/11/01". The output should be like this:

  • {"a", 70, "2019/11/10"}
  • {"b", 30, "2019/11/10"}
  • {"A", 40, "2018/10/05"}
  • {"B", 60, "2018/10/05"}
  • {"xx", 10, "2010/11/01"}
  • {"yy", 90, "2010/11/01"}

I have created the following code:

string dateToReplace = null;
bool earliestFound = false;
foreach (var item in list.OrderByDescending(x => DateTime.Parse(x.start)).ToList())
{
    if (earliestFound) //already marked, safe to remove others
    {
        list.Remove(item);
        continue;
    }

    if (!string.IsNullOrEmpty(dateToReplace)) //remaining matches
    {
        if (string.Equals(item.start, dateToReplace))
        {
            item.start = _earliestDate;
            continue;
        }
        else
        {
            earliestFound = true;
            list.Remove(item);
            continue;
        }
    }
    if (item.start <= _earliestDate) //first earliest or same, more required
    {
        dateToReplace = item.start;
        item.start = _earliestDate;
    }   
}

dateToReplace and earliestFound will be used as flags in the code to determine when to loop.

Somehow I think it is not the best option, do you have any suggestion on how to make it easier to understand or efficient?

DanielV
  • 2,076
  • 2
  • 40
  • 61
  • Does your code works without exception? Usually, it is not allowed to alter a list, while iterating over it at the same time... Regarding your question: a different approach may be the linq Where() method to only fetch the items from the list that should be removed or updated. – Nicolas Nov 26 '18 at 13:45
  • Wow, it looks like a very complicated way to do something very simple. At the same time, I didn't understand what you are trying to achieve. – Yeldar Kurmangaliyev Nov 26 '18 at 13:45
  • 1
    @YeldarKurmangaliyev this seem to be a cleanup of a history alike list – Nicolas Nov 26 '18 at 13:46
  • @DanielV, Instead of removing items from list, can we create new list with filtered results? If Yes then we can do it in one line – Prasad Telkikar Nov 26 '18 at 13:52
  • something like `List result = cc.Where(x => x.start.Date > threshold.Date).ToList();` cc is list of `CostCenter` – Prasad Telkikar Nov 26 '18 at 13:53
  • @Nicolas yes it works without exception, please refer to https://stackoverflow.com/a/10541025/1257607 – DanielV Nov 26 '18 at 13:54
  • 1
    @PrasadTelkikar yes, there is no restriction to remove items from the list – DanielV Nov 26 '18 at 13:55
  • Maybe a typo in your input exemple but 2010/01/01 is less than 2010/11/01 or XX and Yy date changed in the process. – xdtTransform Nov 26 '18 at 13:59
  • @xdtTransform not a typo the earliest date is changed by the variable _earliestDate – DanielV Nov 26 '18 at 14:04
  • But why 100 and 101 Don't get fixed? – xdtTransform Nov 26 '18 at 14:05
  • *get rid of all [...] that have a start date earlier [...] or equal than "2010/11/01"*. xx and yy should be removed since `2010/01/01 <= 2010/11/01` – Cid Nov 26 '18 at 14:08
  • the algorithm should get rid of the ones earlier than the _earliestDate but keep one and replace its value with _earliestDate – DanielV Nov 26 '18 at 14:08
  • As your code is written the rules look like : given a list of date(ListDate) and a "splitDate".Remove all the entry equals to the smallest date if it's inferor to SplitDate. And change every other inferior date to an unrelated Date as "2010/11/01" is not the splitDate nor an existing Date. I mean could you give use the rules not the algorithm and a maching input/output. – xdtTransform Nov 26 '18 at 14:11
  • @xdtTransform i have updated the requirement. SplitDate -> "2010/11/01" – DanielV Nov 26 '18 at 14:43
  • Arg your requirement wasn't clear enought when you post the question. When all your issue was about "fixing" the nearest date after the splitDate. You had only one sentence about itand it was "Fixing" with out the clear condition and the input/output was not Matching. it throw all of us on the wrong track. Explication on code didn't match we had to ignore one. – xdtTransform Nov 26 '18 at 14:57
  • That's say your algo is good the only thing I would change is the final if block I will add an `!flag &&` so once the flag is set you Don't do that test again. – xdtTransform Nov 26 '18 at 14:59
  • remove the continue and add a break. once the Start is strictly inferior to earliestDate you can ignore them and stop looping – xdtTransform Nov 26 '18 at 15:01

5 Answers5

3

To address the first part of your question : Removing items earlier that a given date.

Kiss is your best friend. either you reasign the list after a filtering or you remove all the item you don't want based on a predicate.

var inputs = new List<CostCenter> {
    new CostCenter("a", "70", "2019/11/10"),
    new CostCenter("b", "30", "2019/11/10"),
    new CostCenter("A", "40", "2018/10/05"),
    new CostCenter("B", "60", "2018/10/05"),
    new CostCenter("xx","10", "2010/01/01"),
    new CostCenter("yy", "90", "2010/01/01"),
    new CostCenter("100", "50", "2009/07/03"),
    new CostCenter("101", "50", "2009/07/03"),
};

var result1 = inputs.Where(x => x.start > DateTime.Parse("2010/01/01")).ToList();
var result2 = inputs.RemoveAll(x=> x.start <= DateTime.Parse("2010/01/01"));
xdtTransform
  • 1,986
  • 14
  • 34
  • I may be wrong, but this solution seems to be missing "also this fixed data will replace the earliest match" part. – Yeldar Kurmangaliyev Nov 26 '18 at 14:00
  • @YeldarKurmangaliyev, I din't get this part, if row 100 and 101 disapears without fixing the date. Why are the date of xx and yy changed. – xdtTransform Nov 26 '18 at 14:02
  • @xdtTransform your solution doesn't replace "xx" and "yy" with the start date to "2010/11/01" – DanielV Nov 26 '18 at 14:26
  • Not sure what you mean, sorry – DanielV Nov 27 '18 at 08:44
  • @DanielV, My bad I edited a new answer into this post then edited it out and post it as an other answer but system blocked my new answer. or ajax error .. I don't know but the new code was lost in the process. going thought edit history I got it back and push it as a new answer again . – xdtTransform Nov 27 '18 at 09:39
1

First, you need to filter out all outdated items. Then, you can just take the first start, which is the earliest one, and then replace all with _earliestDate.

if (list == null)
    throw new ArgumentNullException(...);

// Get rid of unused items and sort:
var actual = list
    .Where(item => item.start >= _earliestDate)
    .OrderByDescending(item => item.start)
    .ToArray();

// If there are no matching items, then return empty array or throw exception
// It depends on your requirements
if (actual.Length == 0)
    return actual; 

// Take the least date in resulted array...
var earliestInActual = actual[actual.Length - 1]; 

// ...and replace it with _earliestDate
for (int i = actual.Length - 1; i >= 0; i--)
{
    if (actual[i].start != earliestInActual)
        break;

    item.start = _earliestDate;  
}

return actual;

Please, keep in mind that it will change original objects, therefore start dates will e updated in the original list as well.

Yeldar Kurmangaliyev
  • 33,467
  • 12
  • 59
  • 101
  • I think this is the closest to an answer out of all the ones posted, `item.start > _earliestDate` I think will remove too many results op wants to keep `{"yy", 90, "2010/01/01"}` –  Nov 26 '18 at 14:27
  • @Matt I have updated the answer with `>=`, thanks :) – Yeldar Kurmangaliyev Nov 26 '18 at 15:03
1

Removing items from a list while using a foreach loop will cause it to lose it's position, you can however use a for loop or just do another loop removing all the flagged ones.

DateTime cutOffDate = DateTime.Parse("2010/11/01");

List<CostCenter> removeList = new List<CostCenter>();

foreach (var item in list.OrderByDescending(x => x.start).ToList())
{
    if (item.start <= cutOffDate) {
        removeList.Add(item);
    }
}

foreach(var remove in removeList)
{
    if (list.Contains(remove)) {
        list.Remove(remove);
    }
}
  • Not sure I'm following, how will you lose data? –  Nov 26 '18 at 14:00
  • Your solution is not replacing the earliest match with the cutOffDate, please take a look at the requirement, I have updated the description to make it more clear. – DanielV Nov 26 '18 at 14:46
1

We can create new filtered list with given criteria

        DateTime threshold = new DateTime(2010, 11, 01);
        List<CostCenter> result = cc.Where(x => x.start.Date > threshold.Date).ToList();

POC : .netFiddle

Prasad Telkikar
  • 15,207
  • 5
  • 21
  • 44
1

For a List where 99% of the elements has to be removed, you will currently iterate throught the whole list to find all those elments and removing them.
Instead of clearing the List select only the part you wan't and stop as soon as you have Date inferior to earliestDate. With a "TakeWhile" approach just add a flag to know when to edit the element to fix the date.

public static IEnumerable<CostCenter> TakeWhileAndOneAfter
    (IEnumerable<CostCenter> source, DateTime splitDate )
{
    DateTime? earliestDate = null;
    bool earliestFound = false;

    foreach (CostCenter element in source)
    {
        if (!earliestFound && !(element.start > splitDate))
        {
            earliestFound = true;
            earliestDate = element.start;
        }

        if (earliestFound)
        {
            if (element.start < earliestDate)
            {
                break;
            }
            else
            {
                element.start = splitDate;
            }
        }
        yield return element;
    }
}
xdtTransform
  • 1,986
  • 14
  • 34