LINQ grouping without changing the order [duplicate] - c#

Let's say I have following data:
Time Status
10:00 On
11:00 Off
12:00 Off
13:00 Off
14:00 Off
15:00 On
16:00 On
How could I group that using Linq into something like
[On, [10:00]], [Off, [11:00, 12:00, 13:00, 14:00]], [On, [15:00, 16:00]]

Create a GroupAdjacent extension, such as the one listed here.
And then it's as simple as:
var groups = myData.GroupAdjacent(data => data.OnOffStatus);

You could also do this with one Linq query using a variable to keep track of the changes, like this.
int key = 0;
var query = data.Select(
(n,i) => i == 0 ?
new { Value = n, Key = key } :
new
{
Value = n,
Key = n.OnOffFlag == data[i - 1].OnOffFlag ? key : ++key
})
.GroupBy(a => a.Key, a => a.Value);
Basically it assigns a key for each item that increments when the current item does not equal the previous item. Of course this assumes that your data is in a List or Array, otherwise you'd have to try a different method

Here is a hardcore LINQ solution by using Enumerable.Zip to compare contiguous elements and generate a contiguous key:
var adj = 0;
var t = data.Zip(data.Skip(1).Concat(new TimeStatus[] { null }),
(x, y) => new { x, key = (x == null || y == null || x.Status == y.Status) ? adj : adj++ }
).GroupBy(i => i.key, (k, g) => g.Select(e => e.x));

It can be done as.
Iterate over collection.
Use TakeWhile<Predicate> condition is text of first element of collection On or Off.
Iterate over the subset of from point one and repeat above step and concatenate string.
Hope it helps..

You could parse the list and assign a contiguous key e.g define a class:
public class TimeStatus
{
public int ContiguousKey { get; set; }
public string Time { get; set; }
public string Status { get; set; }
}
You would assign values to the contiguous key by looping through, maintaining a count and detecting when the status changes from On to Off and so forth which would give you a list like this:
List<TimeStatus> timeStatuses = new List<TimeStatus>
{
new TimeStatus { ContiguousKey = 1, Status = "On", Time = "10:00"},
new TimeStatus { ContiguousKey = 1, Status = "On", Time = "11:00"},
new TimeStatus { ContiguousKey = 2, Status = "Off", Time = "12:00"},
new TimeStatus { ContiguousKey = 2, Status = "Off", Time = "13:00"},
new TimeStatus { ContiguousKey = 2, Status = "Off", Time = "14:00"},
new TimeStatus { ContiguousKey = 3, Status = "On", Time = "15:00"},
new TimeStatus { ContiguousKey = 3, Status = "On", Time = "16:00"}
};
Then using the following query you can extract the Status and grouped Times:
var query = timeStatuses.GroupBy(t => t.ContiguousKey)
.Select(g => new { Status = g.First().Status, Times = g });

Related

List of numbers lowest value first, and then by input date

So I have a list of prices from a database. I would like to sort it so that the first entry in a list is the entry with the lowest number. And then all other entry are order by input date.
How can this be done?
This is my code, which is a mess, sorry I'm trying stuff :)
var itemPriceDate = itemPrice.OrderBy(d => d.Invoice.DateInvoice).ToList();
var itemPriceDateLow= itemPriceDate.OrderBy(c => c.qtPrice).ThenBy(d => d.Invoice.DateInvoice);
ViewBag.ItemPrice = itemPriceDateLow; ```
First find out the lowest value from the List(itemPrice).
double lowest_price = itemPrice.Min(c => c.qtPrice);
Next, remove the lowest element from the list.
var itemToRemove = itemPrice.Single(c => c.qtPrice == lowest_price);
itemPrice.Remove(itemToRemove);
Next, sort the remaining list based on input Date.
var newList = itemPrice.OrderByDescending(d => d.Invoice.DateInvoice).ToList();
Finally, add lowest element at first index
newList.Insert(0, lowest_price);
LINQ is great when it works, but it sometimes does unexpected things. Depending on how large your dataset is, you may be better off doing it as a stored procedure that returns the data already ordered.
If the dataset is small or you're cornered into using C# to do it there is the option of using a custom sort function. Without knowing the exact structure of your data, this is more intended as a blanket example that will need tweaking accordingly.
Let's say your list is stored in the itemPrice variable, if you do something along the lines of:
itemPrice.Sort((a, b) => {
int retVal = a.qtPrice < b.qtPrice;
return ret != 0 ? ret : a.Invoice.DateInvoice < b.Invoice.DateInvoice;
});
Will sort by qtPrice and then fall back to the DateInvoice field; you may need to swap the less than to a greater than to get your desired order.
One sort is enough. What I think it should be is:
var itemPriceDateLow= itemPriceDate.OrderBy(c => c.qtPrice).ThenBy(d => d.Invoice.DateInvoice);
This will obviously give you whole collection. You might want to use .First() if you want to get top most element.
One thing to remember - ThenBy, OrderBy are ascending by default.
Take a look at this example:
class Program
{
static void Main(string[] args)
{
List<ItemPrice> items = new List<ItemPrice>();
items.Add(new ItemPrice() { Date = DateTime.Now, QtyPrice = 1});
items.Add(new ItemPrice() { Date = DateTime.Now.AddDays(-1), QtyPrice = 1});
items.Add(new ItemPrice() { Date = DateTime.Now, QtyPrice = 2});
var sortedItem = items.OrderBy(p => p.QtyPrice).ThenBy(p => p.Date).First();
Console.WriteLine($"Default Ascending sort {sortedItem.Date}, {sortedItem.QtyPrice}");
var sortedItemWithReverseDate = items.OrderBy(p => p.QtyPrice).ThenByDescending(p => p.Date).First();
Console.WriteLine($"Descending sort on date {sortedItemWithReverseDate.Date}, {sortedItemWithReverseDate.QtyPrice}");
}
}
class ItemPrice {
public DateTime Date { get; set; }
public decimal QtyPrice { get; set; }
}
It will give you:
Default Ascending sort 16/08/2021 12:47:34, 1
Descending sort on date 17/08/2021 12:47:34, 1
You would need to iterate the collection twice in this case, since you would first need to know the Aggregate Value (Min). Then, you could use a Custom Comparer as the following.
public class CustomComparer : IComparer<Item>
{
private int _minValue;
public CustomComparer(int minValue)
{
_minValue= minValue;
}
public int Compare(Item instanceA, Item instanceB)
{
if(instanceA.Price == _minValue) return -1;
if(instanceB.Price == _minValue) return 1;
return instanceA.InputDate.CompareTo(instanceB.InputDate);
}
}
Now you can fetch the result as
var min = list.Min(x=>x.Price);
var result = list.OrderBy(x=>x,new CustomComparer(min));
Example,
public class Item
{
public int Price{get;set;}
public DateTime InputDate{get;set;}
}
var list = new List<Item>
{
new Item{Price = 2, InputDate=new DateTime(2021,3,1)},
new Item{Price = 12, InputDate=new DateTime(2021,7,1)},
new Item{Price = 12, InputDate=new DateTime(2021,9,1)},
new Item{Price = 42, InputDate=new DateTime(2021,1,1)},
new Item{Price = 32, InputDate=new DateTime(2021,6,1)},
new Item{Price = 22, InputDate=new DateTime(2021,4,1)},
new Item{Price = 2, InputDate=new DateTime(2021,3,2)},
new Item{Price = 12, InputDate=new DateTime(2021,2,1)}
};
var min = list.Min(x=>x.Price);
var result = list.OrderBy(x=>x,new CustomComparer(min));
Output
Thx for all your inputs.
For me the right way to go was.
Order my "itemPrice" list by "OrderByDescending(by date)"
Then find out the lowest value from the List(itemPrice).
double lowest_price = itemPrice.Min(c => c.qtPrice);
Then declare a new List
List<qtInvoice> newItemPrice = new List<qtInvoice>();
First loop that adds all the "lowest_price" to "newItemPrice" list
foreach (var item in itemPriceDate)
{
if (item.qtPrice == lowest_price)
{
newItemPrice.Add(item);
}
}
Then in second loop you add all the rest of the prices to "newItemPrice" list
foreach (var item in itemPriceDate)
{
if (item.qtPrice != lowest_price)
{
newItemPrice.Add(item);
}
}

C# sort object list with start position and loop

I have a strange question :)
I have a object list looking like this:
var list = new []
{
new { Id = 1, Name = "Marcus" },
new { Id = 2, Name = "Mattias" },
new { Id = 3, Name = "Patric" },
new { Id = 4, Name = "Theodor" },
};
I would like to sort the list providing a "start id"
For example, if I provide "start id" 3, the result should look like this:
Id
Name
3
Patric
4
Theodor
1
Marcus
2
Mattias
I have no idea where to start, so I really need some help from you coding gods
The list is from a sql table, but it does not matter for me where the sort take place (in sql query or in c# code)
Try this:
var list = new []
{
new { Id = 1, Name = "Marcus" },
new { Id = 2, Name = "Mattias" },
new { Id = 3, Name = "Patric" },
new { Id = 4, Name = "Theodor" },
};
var start_id = 3;
var max_id = list.Max(y => y.Id);
var result =
from x in list
orderby (x.Id + max_id - start_id) % max_id
select x;
I get:
With LINQ to objects you can do something like that:
var list = new []
{
new { Id = 1, Name = "Marcus" },
new { Id = 2, Name = "Mattias" },
new { Id = 3, Name = "Patric" },
new { Id = 4, Name = "Theodor" },
};
var startId = 3;
var result = list
.GroupBy(i => i.Id >= startId ? 1 : 0) // split in two groups
.OrderByDescending(g => g.Key) // sort to have the group with startId first
.Select(g => g.OrderBy(i => i.Id)) // sort each group
.SelectMany(i => i) // combine result
.ToList();
Console.WriteLine(string.Join(", ", result.Select(i => i.Id))); // prints "3, 4, 1, 2"
You require 2 criteria to apply:
Order ascending by Id.
Return the Ids greater than threshold before the Ids less than threshold.
You can try:
var offset = 3;
var sorted1 = list
.OrderBy(item => item.Id < offset)
.ThenBy(item => item.Id);
The OrderBy condition yields true if Id is less than offset and false otherwise.
true is greater than false and therefore is returned later
A dirty way could also be:
var offset = 3;
var sorted2 = list
.OrderBy(item => unchecked((uint)(item.Id - offset)));
Here the offset is subtracted from Id and the result converted to unsigned int to make the negative values become very large positive ones. A little hacky. Might not work with queries against SQL providers.
Here's a toy Non-Linq Version
object[] ShiftList(int id)
{
var list = new dynamic[]
{
new { Id = 1, Name = "Marcus" },
new { Id = 2, Name = "Mattias" },
new { Id = 3, Name = "Patric" },
new { Id = 4, Name = "Theodor" },
};
Span<dynamic> listSpan = list;
int indexFound = -1;
for (int i = 0; i < list.Length; i++)
{
if (listSpan[i].Id == id)
{
indexFound = i;
}
}
if (indexFound is -1)
{
return list;
}
var left = listSpan.Slice(0, indexFound);
var right = listSpan[indexFound..];
object[] objs = new object[list.Length];
Span<object> objSpan = objs;
right.CopyTo(objSpan);
left.CopyTo(objSpan[right.Length..]);
return objs;
}
Try using foreach and iterate over each object in your list:
foreach (var item in list)
{
}
from here you should be able to use some of the collection methods for a list to reorder your list.

Group and Merge multiple values from single List using LINQ

I have the following enums and class
internal enum flag_dead_alive
{
none = 0
,
dead = 1
,
alive = 2
}
internal enum flag_blocked_unblocked
{
none = 0
,
blocked = 1
,
unblocked = 2
}
internal class cls_entry
{
internal string id_number { get; set; }
internal flag_dead_alive dead_alive { get; set; }
internal flag_blocked_unblocked blocked_unblocked { get; set; }
}
I have List which contains hundreds of thousands of records, so for testing purposes I have created a sample list that contain the same sort of records, below (id_number is deliberately set as string for reasons that are irrelevant right now)
List<cls_npr_entry> output = new List<cls_npr_entry>()
{
new cls_npr_entry() { id_number = "1", dead_alive = flag_dead_alive.alive, blocked_unblocked = flag_blocked_unblocked.none }
,
new cls_npr_entry() { id_number= "1", dead_alive = flag_dead_alive.none, blocked_unblocked= flag_blocked_unblocked.blocked }
,
new cls_npr_entry(){id_number= "2", dead_alive = flag_dead_alive.none, blocked_unblocked= flag_blocked_unblocked.blocked }
,
new cls_npr_entry(){id_number= "3", dead_alive = flag_dead_alive.dead, blocked_unblocked= flag_blocked_unblocked.none }
,
new cls_npr_entry(){id_number= "3", dead_alive = flag_dead_alive.none, blocked_unblocked= flag_blocked_unblocked.unblocked }
};
From the list, I want to get output of grouped and merged (is that the correct term here?) records from my list. However, any enum that is set to "none" should be discarded if a matched record has a different value to "none", otherwise it must remain "none". For example, the output for the above list should be
1 : id_number = 1, dead_alive = alive, blocked_unblocked = blocked
2 : id_number = 2, dead_alive = none, blocked_unblocked = blocked
3 : id_number = 3, dead_alive = dead, blocked_unblocked = unblocked
The code
var groups = output.GroupBy(x => x.id_number);
returns the records in the correct groups, but I have no idea where to from here. I also have
var groups = output.GroupBy(x => x.id_number).Select(g => g.Select(y => new { y.id_number, y.blocked_unblocked, y.dead_alive }));
but that returns the same result as the first query. I need to figure out how to select one record from y.dead_alive and one record from y.blocked_unblocked, so that the result only returns only the relevant record to create one record from both.
All help would be appreciated.
For your outputtest list, you can get theMax of dead_alive and blocked_unblocked after grouping, like the following code:
var groups = output.GroupBy(x => x.id_number)
.Select(y => new cls_entry
{
id_number = y.Key,
dead_alive = y.Max(e => e.dead_alive),
blocked_unblocked = y.Max(e => e.blocked_unblocked)
}).ToList();
Documentation of Max method : Max

Convert list entries to array

In order to generate a graph using d3 I need to convert my list of time entries to several arrays.
I store my data in a list of work records per day per staff
I need to be able to get an array of all days, and then a array each per member of staff.
So lets say staff x has 3.5h against 01/1/19 and 4.5h against 03/1/19
Staff y has 6h agaist 2/1/19
I'd expect my arrays to look as following:
Dates[1/1/19, 2/1/19, 3/1/19]
X[3.5,0,4.5]
Y[0,6,0]
Some of my code is:
public IEnumerable<TicketWorkRecord> TimeByDateByStaff { get; set; }
public class TicketWorkRecord
{
public DateTime Date { get; set; }
public decimal TimeSpent { get; set; }
}
Assuming you have a class called StaffMember that looks like this:
public class StaffMember
{
public IEnumerable<TicketWorkRecord> TimeByDateByStaff { get; set; }
// Other properties
}
And after adding the following constructor to your TicketWorkRecord class:
public TicketWorkRecord(DateTime date, decimal timeSpent)
{
Date = date;
TimeSpent = timeSpent;
}
Let's create a dummy data for X and Y staff members:
StaffMember X = new StaffMember
{
TimeByDateByStaff = new List<TicketWorkRecord>()
{
new TicketWorkRecord(DateTime.Today.Date, 3.5M),
new TicketWorkRecord(DateTime.Today.Date.AddDays(2), 4.5M)
}
};
StaffMember Y = new StaffMember
{
TimeByDateByStaff = new List<TicketWorkRecord>()
{ new TicketWorkRecord(DateTime.Today.Date.AddDays(1), 6M) }
};
var staffMembers = new List<StaffMember>() { X, Y };
Now, you can construct your desired 3 arrays using the following code:
var dates = staffMembers.SelectMany(s => s.TimeByDateByStaff)
.Select(t => t.Date)
.Distinct()
.OrderBy(d => d).ToArray();
var xTimes = dates.Select(d => X.TimeByDateByStaff
.FirstOrDefault(t => t.Date == d)?.TimeSpent ?? 0).ToArray();
var yTimes = dates.Select(d => Y.TimeByDateByStaff
.FirstOrDefault(t => t.Date == d)?.TimeSpent ?? 0).ToArray();
To test it:
Console.WriteLine("Dates: " + string.Join(",", dates.Select(d => d.ToShortDateString())));
Console.WriteLine("xTimes: " + string.Join(",", xTimes));
Console.WriteLine("yTimes: " + string.Join(",", yTimes));
Output:
Dates: 12/01/2019,13/01/2019,14/01/2019
xTimes: 3.5,0,4.5
yTimes: 0,6,0
If TicketWorkRecord has a property specifying which staff member it is (X or Y), then this would be pretty straight forward using LINQ:
var dates = TimeByDateByStaff.Select(x => x.Date.ToString("MM/dd/yy")).ToArray();
var staffXTimeSpent = TimeByDateByStaff.Select(x => x.StaffMember == "X" ? x.TimeSpent : 0M).ToArray();
var staffYTimeSpent = TimeByDateByStaff.Select(x => x.StaffMember == "Y" ? x.TimeSpent : 0M).ToArray();
Alternatively, if the exact staff members aren't known at compile time then you can get the time entries by staff member at runtime:
var timeSpentByStaffMembers = TimeByDateByStaff
.Select(x => x.StaffMember)
.Distinct()
.ToDictionary(
key => key,
value => TimeByDateByStaff.Select(x => x.StaffMember == value ? x.TimeSpent : 0M).ToArray());
With the following data:
var TimeByDateByStaff = new List<TicketWorkRecord>
{
new TicketWorkRecord
{
Date = new DateTime(2019, 1, 1),
TimeSpent = 3.5M,
StaffMember = "X"
},
new TicketWorkRecord
{
Date = new DateTime(2019, 2, 1),
TimeSpent = 6M,
StaffMember = "Y"
},
new TicketWorkRecord
{
Date = new DateTime(2019, 3, 1),
TimeSpent = 4.5M,
StaffMember = "X"
}
};
The LINQ statements above produce the following output:
If i understand you correctly, you want to split your list of objects into individuals fields arrays.
If yes, Lets say you have the following list
List<Object> ObjectsList = ObjectsList;
string[] ExtractDates = ObjectsList.Select(x=>x.Date).ToArray();
double[] TimeSpent = ObjectsList.Select(x=> x.TimeSpent).ToArray();
and so forth, you can apply where condition to filter based on members

Tricky combination of two lists using Linq

I hope somebody will be able to guide me in right direction here...
public class SubmissionLog
{
public int PKId {get;set;}
public int SubmissionId {get;set;}
public DateTime Created {get;set;}
public int StatusId {get;set;}
}
And this is the data:
1, 123, '1/24/2013 01:00:00', 1
2, 456, '1/24/2013 01:30:00', 1
3, 123, '1/25/2013 21:00:00', 2
4, 456, '1/25/2013 21:30:00', 2
5, 123, '2/25/2013 22:00:00', 1
6, 123, '2/26/2013 21:00:00', 2
7, 123, '2/16/2013 21:30:00', 1
What I am trying to is following:
I'd like to know the the average time span from StatusId 1 to StatusId 2 on a given day.
So, let's say date is 2/26/2013, then what I thought would make sense if first get the list like this:
var endlingList = (from sl in db.SubmissionLogs
where (DateTime.Now.AddDays(days).Date == sl.Created.Date) // days = passed number of days to make it 2/26/2013
&& (sl.StatusId == 2)
select sl).ToList();
var endingLookup = endlingList.ToLookup(a => a.SubmissionId, a => a.Created); // thought of using lookup because Dictionary doesn't allow duplicates
After that I thought I'd figure out starting points
var startingList = (from sl in db.SubmissionLogs
where endingList.Select(a => a.SubmissionId).ToArray().Contains(sl.QuoteId)
&& sl.StatusId == 1
select sl).ToList();
And then what I did was following:
var revisedList = endingLookup.Select(a =>
new SubmissionInterval {
SubmissionId = a.Key,
EndDateTime = endingLookup[a.Key].FirstOrDefault(), //This is where the problem is. This will only grab the first occurance.
StartDateTime = startLookup[a.Key].FirstOrDefault() //This is where the problem is. This will only grab the first occurance.
});
And then what I do to get average is following (again, this will only include the initial or first ocurances of status 1 and status 2 of some submission id Submission Log):
return revisedList.Count() > 0 ? revisedList.Select(a=> a.EndDateTime.Subtract(a.StartDateTime).TotalHours).Average() : 0;
So, I hope somebody will understand what my problem here is first of all... To re-cap, I want to get timespan between each status 1 and 2. I pass the date in, and then I have to look up 2's as that ensures me that I will find 1's. If I went the other way around and looked for 1's, then 2's may not exist (don't want that anyway).
At the end I wanna be able to average stuff out...
So let's say if some submission first went from 1 to 2 in a time span of 5h (the code that I left, will get me up to this point), then let's say it got reassigned to 1 and then it went back to 2 in a new time span of 6h, I wanna be able to get both and do the average, so (5+6)/2.
Thanks
I think I understand what you're trying to do. Does thishelp
void Main()
{
var list = new List<SubmissionLog>
{
new SubmissionLog(1, 123, "1/24/2013 01:00:00", 1),
new SubmissionLog(2, 456, "1/24/2013 01:30:00", 1),
new SubmissionLog(3, 123, "1/25/2013 21:00:00", 2),
new SubmissionLog(4, 456, "1/25/2013 21:30:00", 2),
new SubmissionLog(5, 123, "2/25/2013 22:00:00", 1),
new SubmissionLog(6, 123, "2/26/2013 21:00:00", 2),
new SubmissionLog(7, 123, "2/16/2013 21:30:00", 1),
};
// split out status 1 and 2
var s1s = list.Where (l => l.StatusId == 1).OrderBy (l => l.Created);
var s2s = list.Where (l => l.StatusId == 2).OrderBy (l => l.Created);
// use a sub-query to get the first s2 after each s1
var q = s1s.Select (s1 => new
{
s1,
s2 = s2s.FirstOrDefault (s2 =>
s1.SubmissionId == s2.SubmissionId &&
s2.Created >= s1.Created
)
}
).Where (s => s.s1.PKId < s.s2.PKId && s.s2 != null);
// extract the info we need
// note that TotalSecond is ok in Linq to Object but you'll
// probably need to use SqlFunctions or equivalent if this is to
// run against a DB.
var q1 = q.Select (x => new
{
Start=x.s1.Created,
End=x.s2.Created,
SubmissionId=x.s1.SubmissionId,
Seconds=(x.s2.Created - x.s1.Created).TotalSeconds
}
);
// group by submissionId and average the time
var q2 = q1.GroupBy (x => x.SubmissionId).Select (x => new {
x.Key,
Count=x.Count (),
Start=x.Min (y => y.Start),
End=x.Max (y => y.End),
Average=x.Average (y => y.Seconds)});
}
public class SubmissionLog
{
public SubmissionLog(int id, int submissionId, string date, int statusId)
{
PKId = id;
SubmissionId = submissionId;
Created = DateTime.Parse(date, CultureInfo.CreateSpecificCulture("en-US"));
StatusId = statusId;
}
public int PKId {get;set;}
public int SubmissionId {get;set;}
public DateTime Created {get;set;}
public int StatusId {get;set;}
}

Categories

Resources