This is a little hard to explain. I have a datatable with schedule information. Each row represents a schedule with a start date/time and an end date/time. I need to group these such that the overall start to end time matches a given duration.
For example, I might have the following in my datatable:
Schedule1: Start - 9:00AM, End - 9:30AM
Schedule2: Start - 9:30AM, End - 10:00AM
Schedule3: Start - 10:00AM, End - 10:30AM
Schedule4: Start - 10:30AM, End - 11:00AM
Now if I'm given a duration value of 60 min, then I need to be able to produce the following as output:
Block1: Schedules(1,2): 9:00AM - 10:00AM
Block2: Schedules(2,3): 9:30AM - 10:30AM
Block3: Schedules(3,4): 10:00AM - 11:00AM
If however the duration was instead 120 min, then I would need to produce the following:
Block1: Schedules(1,2,3,4): 9:00AM - 11:00AM
Let me know if this needs clarification. I need to write a method in C# to do this conversion. Please help me with this as I've been stuck on it for a long time.
Whether you choose to do this in C# or SQL depends partly on the scale of the data. Assuming that we're working with a relatively small number of time ranges (say < 10), it would be reasonable to pull all the times into memory and find the blocks in C#.
Given the following classes:
public class Schedule {
public int ID { get; set; }
public DateTime Start { get; set; }
public DateTime End { get; set; }
public int Minutes { get; set; }
}
public class ScheduleBlock : Schedule {
public List<Schedule> Schedules { get; set; }
}
Here is a simple algorithm that iteratively combines ranges together until every possible combination is represented (note that the number of combinations grows as O(n^2)):
public List<ScheduleBlock> CombineAllSchedules(List<Schedule> origschedules, out int added)
{
added = 0;
var schedules = new List<ScheduleBlock>();
foreach (var s in origschedules) {
var snew = new ScheduleBlock { Schedules = new List<Schedule> { s }, Start = s.Start, End = s.End, Minutes = s.Minutes };
schedules.Add(snew);
}
for (var i = 0; i < schedules.Count; i++) {
var s = schedules[i];
var matchstart = schedules.Where (s2 => s2.End == s.Start).ToList();
var matchend = schedules.Where (s2 => s2.Start == s.End).ToList();
foreach (var s2 in matchstart) {
var newschedule = CombineSchedules(s2, s);
if (!schedules.Any (sc => sc.Start == newschedule.Start && sc.End == newschedule.End)) {
schedules.Add(newschedule);
added++;
}
}
foreach (var s2 in matchend) {
var newschedule = CombineSchedules(s, s2);
if (!schedules.Any (sc => sc.Start == newschedule.Start && sc.End == newschedule.End)) {
schedules.Add(newschedule);
added++;
}
}
}
return schedules;
}
public ScheduleBlock CombineSchedules(Schedule s1, Schedule s2)
{
var schedules = new List<Schedule>();
if (s1 is ScheduleBlock) schedules.AddRange(((ScheduleBlock)s1).Schedules);
else schedules.Add(s1);
if (s2 is ScheduleBlock) schedules.AddRange(((ScheduleBlock)s2).Schedules);
else schedules.Add(s2);
var s = new ScheduleBlock {
Schedules = schedules,
Start = s1.Start, End = s2.End, Minutes = s1.Minutes + s2.Minutes
};
return s;
}
Once the combinations are put together, then it is a simple matter to query them and get specific lengths (like 60 minutes or 120 minutes):
public List<ScheduleBlock> FindBlocks(List<Schedule> schedules, int blockLength)
{
int added;
var combinedSchedules = CombineAllSchedules(schedules, out added);
var result = combinedSchedules.Where (s => s.Minutes == blockLength).ToList();
return result;
}
With this algorithm in place, you can do something like this for example to get the output you're looking for:
var schedules = new List<Schedule> {
new Schedule { ID = 1, Start = DateTime.Parse("09:00 AM"), End = DateTime.Parse("09:30 AM") },
new Schedule { ID = 2, Start = DateTime.Parse("09:30 AM"), End = DateTime.Parse("10:00 AM") },
new Schedule { ID = 3, Start = DateTime.Parse("10:00 AM"), End = DateTime.Parse("10:30 AM") },
new Schedule { ID = 4, Start = DateTime.Parse("10:30 AM"), End = DateTime.Parse("11:00 AM") },
};
foreach (var s in schedules) {
s.Minutes = (int)(s.End - s.Start).TotalMinutes;
}
Console.WriteLine("60 Minute Blocks");
Console.WriteLine("----------------");
var blocks = FindBlocks(schedules, 60);
var blockId = 1;
foreach (var block in blocks) {
var output = "Block" + blockId +
": Schedules(" + string.Join(",", block.Schedules.Select (s => s.ID)) + "): " +
block.Start.ToString("h:mmtt") + " - " + block.End.ToString("h:mmtt");
Console.WriteLine(output);
blockId++;
}
Console.WriteLine();
Console.WriteLine("120 Minute Blocks");
Console.WriteLine("----------------");
blocks = FindBlocks(schedules, 120);
blockId = 1;
foreach (var block in blocks) {
var output = "Block" + blockId +
": Schedules(" + string.Join(",", block.Schedules.Select (s => s.ID)) + "): " +
block.Start.ToString("h:mmtt") + " - " + block.End.ToString("h:mmtt");
Console.WriteLine(output);
blockId++;
}
Sample Result:
60 Minute Blocks
----------------
Block1: Schedules(1,2): 9:00AM - 10:00AM
Block2: Schedules(2,3): 9:30AM - 10:30AM
Block3: Schedules(3,4): 10:00AM - 11:00AM
120 Minute Blocks
----------------
Block1: Schedules(1,2,3,4): 9:00AM - 11:00AM
Related
I have this price range, formed in a string like this:
100:105 , 110:120 , 150:200 , 240:245
This means the prices range from
100 to 105
and
110 to 120
and
150 to 200
and
240 to 245
I want to check if a new price range is within our acceptable price range. Here is my code so far:
int ibegin = 110
int iend = 115
string BeginEnd = "100:105,110:120,150:200,240:245";
string[] strBeginEnd = BeginEnd.Split(',');
int pos = Array.IndexOf(strBeginEnd, ibegin+":"+ iend);
if (pos > -1)
{
RepText = RepText + ScriptBefore + TextIn + ScriptAfter + TextAfter;
}
This code works if the price start and end matches the start and end of the range.
For example, if ibegin = 110 and iend = 120 then my code will find it based on if 110:120 exists in the list.
However, if the iend = 115 it won't find it.
public static void Main(string[] args) {
string BeginEnd = "100:105,110:120,150:200,240:245";
Dictionary<string, int[]> data = new Dictionary<string, int[]>();
foreach (var val in BeginEnd.Split(",")) {
var start = int.Parse(val.Split(":")[0]);
var end = int.Parse(val.Split(":")[1]);
var range = new int[end - start+1];
for (var i = 0; i < range.Length; i++) {
range[i] = start + i;
}
data.Add(val, range);
}
var test1 = check(110);
var test2 = check(115);
var test3 = check(200);
var test4 = check(240);
Console.WriteLine("test1 {0} ,test2 {1} ,test3 {2} ,test4 {3} ",test1,test2,test3,test4);
string check(int value) => data.FirstOrDefault(pair => pair.Value.Contains(value)).Key;
}
You need to split the ranges, parse the numbers and check them:
var search = new {from = 110, to= 115};
var found = "100:105,110:120,150:200,240:245"
.Split(',') //split in parts like 100:105
.Select(p => {
var parts = p.Split(':'); // split the range from/to
var from = int.Parse(parts[0]); //convert from/to to ints
var to = int.Parse(parts[1]);
return new { from, to };
})
//check if any range matches the search values
.Any(range => search.from >= range.from && search.to <= range.to);
Console.WriteLine($"Found: {found}");
bool IsInRange(int ibegin, int iend, string BeginEnd)
{
string[] strBeginEnd = BeginEnd.Split(',');
foreach (var range in strBeginEnd)
{
var rangeArray = range.Split(':');
if (ibegin >= int.Parse(rangeArray[0]) && iend <= int.Parse(rangeArray[1]))
{
return true;
}
}
return false;
}
I'm working on a scheduling algorithm that generates/assigns time-slots to a List of Recipients based on the following restrictions:
Max Recipients per Minute
Max Recipients per Hour
Suppose that the delivery Start Time is 2018-10-17 9:00 AM and we have 19 recipients with Max of 5 per min and and 10 per hour, so the output should be:
5 Recipients will be scheduled on 2018-10-17 9:00 AM
5 Recipients will be scheduled on 2018-10-17 9:01 AM
5 Recipients will be scheduled on 2018-10-17 10:00 AM
4 Recipients will be scheduled on 2018-10-17 10:01 AM
The algorithm is very accurate, but the way it works is as following:
First it generates a list of time-slots or time-windows that are accurately fits the no. of recipients based on the restrictions i mentioned before.
then, I'm moving whatever available in the List of Time-Slots for each set/group or recipients.
in the list of Time-Slots I added a counter that increments for every recipient added to it, so in this way I can track the no. of each recipients added to each time-slot to respect the Max per Min./Hr restrictions.
The previous process it simplified in this code snippet - I'm using While Loop to iterate, in my case when having 500K recipients this is taking 28 minutes to get it done!
I tried to use Parallel.ForEach but I couldn't figure out how to implement it in this case.
DateTime DeliveryStart = DateTime.Now;
//This list has DateTime: Time-windows values starting from DeliveryStart to the Max value of the time needed to schedule the Recipients
var listOfTimeSlots = new List<Tuple<DateTime, bool, int>>();
//List of Recipients with Two types of data: DateTime to tell when its scheduled and int value refers to the Recipient's ID
var ListOfRecipients = new List<Tuple<DateTime, int>>();
List<Tuple<int, DateTime>> RecipientsWithTimeSlots= new List<Tuple<int, DateTime>>();
int noOfRecipients = ListOfRecipients.Count;
int Prevhour = 0, _AddedPerHour = 0, Prevday = 0;
// Scheduling restrictions
int _MaxPerHour = 5400, _MaxPerMinute = 90;
int i = 0;
int indexStart = 0;
// ...
// ...
// Code to fill listOfTimeSlots ListOfRecipients with Data
while (noOfRecipients > 0)
{
var TimeStamp = listOfTimeSlots[i];
int hour = TimeStamp.Item1.Hour;
int day = TimeStamp.Item1.Day;
if (Prevhour == 0)
{
Prevhour = hour;
Prevday = day;
}
if (Prevhour != hour)
{
Prevhour = hour;
_AddedPerHour = 0;
}
if (_AddedPerHour >= _MaxPerHour)
{
var tmpItem = listOfTimeSlots.Where(l => l.Item1.Hour == hour && l.Item1.Day == day).LastOrDefault();
int indexOfNextItem = listOfTimeSlots.LastIndexOf(tmpItem) + 1;
i = indexOfNextItem;
_AddedPerHour = 0;
continue;
}
else
{
int endIndex;
endIndex = _MaxPerMinute > noOfRecipients ? noOfRecipients : _MaxPerMinute;
if (endIndex > Math.Abs(_AddedPerHour - _MaxPerHour))
endIndex = Math.Abs(_AddedPerHour - _MaxPerHour);
var RecipientsToIteratePerMinute = ListOfRecipients.GetRange(indexStart, endIndex);
foreach (var item in RecipientsToIteratePerMinute)
{
RecipientsWithTimeSlots.Add(new Tuple<int, DateTime>(item.Item2, TimeStamp.Item1));
listOfTimeSlots[i] = new Tuple<DateTime, bool, int>(TimeStamp.Item1, true, listOfTimeSlots[i].Item3 + 1);
_AddedPerHour++;
}
indexStart += endIndex;
noOfRecipients -= endIndex;
i++;
}
}
I simplified the code in here, for not making it so complex to understand, all i want it to speed-up the while loop or replacing it with a Parallel.ForEach.
THE WHILE LOOP IS NEVER SIMPLIFIED, THIS IS HOW IT EXACTLY WORKS \
Any help or suggestion is appreciated.
Here is a different approach. It creates the groups of ids first, then assigns them the date based on the requirements.
First, a class to represent the groups (avoid them tuples):
public class RecipientGroup
{
public RecipientGroup(DateTime scheduledDateTime, IEnumerable<int> recipients)
{
ScheduledDateTime= scheduledDateTime;
Recipients = recipients;
}
public DateTime ScheduledDateTime { get; private set; }
public IEnumerable<int> Recipients { get; private set; }
public override string ToString()
{
return string.Format($"Date: {ScheduledDateTime.ToShortDateString()} {ScheduledDateTime.ToLongTimeString()}, count: {Recipients.Count()}");
}
}
Then a class to iterate through the groups. You will see why this is needed later:
public class GroupIterator
{
public GroupIterator(DateTime scheduledDateTime)
{
ScheduledDateTime = scheduledDateTime;
}
public DateTime ScheduledDateTime { get; set; }
public int Count { get; set; }
}
Now, the code:
DateTime DeliveryStart = new DateTime(2018, 10, 17);
//List of Recipients (fake populate function)
IEnumerable<int> allRecipients = PopulateRecipients();
// Scheduling restrictions
int maxPerMinute = 90;
int maxPerHour = 270;
//Creates groups broken down by the max per minute.
var groupsPerMinute = allRecipients
.Select((s, i) => new { Value = s, Index = i })
.GroupBy(x => x.Index / maxPerMinute)
.Select(group => group.Select(x => x.Value).ToArray());
//This will be the resulting groups
var deliveryDateGroups = new List<RecipientGroup>();
//Perform an aggregate run on the groups using the iterator
groupsPerMinute.Aggregate(new GroupIterator(DeliveryStart), (iterator, ids) =>
{
var nextBreak = iterator.Count + ids.Count();
if (nextBreak >= maxPerHour)
{
//Will go over limit, split
var difference = nextBreak-maxPerHour;
var groupSize = ids.Count() - difference;
//This group completes the batch
var group = new RecipientGroup(iterator.ScheduledDateTime, ids.Take(groupSize));
deliveryDateGroups.Add(group);
var newDate = iterator.ScheduledDateTime.AddHours(1).AddMinutes(-iterator.ScheduledDateTime.Minute);
//Add new group with remaining recipients.
var stragglers = new RecipientGroup(newDate, ids.Skip(groupSize));
deliveryDateGroups.Add(stragglers);
return new GroupIterator(newDate, difference);
}
else
{
var group = new RecipientGroup(iterator.ScheduledDateTime, ids);
deliveryDateGroups.Add(group);
iterator.ScheduledDateTime = iterator.ScheduledDateTime.AddMinutes(1);
iterator.Count += ids.Count();
return iterator;
}
});
//Output minute group count
Console.WriteLine($"Group count: {deliveryDateGroups.Count}");
//Groups by hour
var byHour = deliveryDateGroups.GroupBy(g => new DateTime(g.ScheduledDateTime.Year, g.ScheduledDateTime.Month, g.ScheduledDateTime.Day, g.ScheduledDateTime.Hour, 0, 0));
Console.WriteLine($"Hour Group count: {byHour.Count()}");
foreach (var group in byHour)
{
Console.WriteLine($"Date: {group.Key.ToShortDateString()} {group.Key.ToShortTimeString()}; Count: {group.Count()}; Recipients: {group.Sum(g => g.Recipients.Count())}");
}
Output:
Group count: 5556
Hour Group count: 1852
Date: 10/17/2018 12:00 AM; Count: 3; Recipients: 270
Date: 10/17/2018 1:00 AM; Count: 3; Recipients: 270
Date: 10/17/2018 2:00 AM; Count: 3; Recipients: 270
Date: 10/17/2018 3:00 AM; Count: 3; Recipients: 270
Date: 10/17/2018 4:00 AM; Count: 3; Recipients: 270
Date: 10/17/2018 5:00 AM; Count: 3; Recipients: 270
... and so on for all 1852 groups.
This takes about 3 seconds to complete.
I am sure there are edge cases. I wrote this in a hurry so just think about those.
I'm struggling to get my head round this one.
I have a List<Appointment> which contains DateTime Start and DateTime End along with a few others bits.
I want to draw a day planner which lists all of the appointments in order with placeholders/gaps where appropriate.
I have managed this, but now I need to handle the scenario where appointments overlap. It seems this could be;
appointment a starts before and ends after appointment b
appointment a starts before and ends during appointment b
appointment a starts during and end after appointment b
appointment a start during and ends during appointment b
This feels like a problem which must have been tackled before. Any pointers?
Here's an example of some horrendous code i'm currently working on;
List<TechActivityModel> techActivity = new TechService().GetTechActivity(7, new DateTime(2018, 4, 11), new DateTime(2018, 4, 11).AddDays(1))
.OrderBy(t => t.Start).ToList();
for (int i = techActivity.Count - 1; i >= 0; i--)
{
//something starts before and ends after
TechActivityModel clash = techActivity.Where(t => t.Id != techActivity[i].Id && t.Start < techActivity[i].Start && t.End > techActivity[i].End).FirstOrDefault();
while (clash != null)
{
//split the clashing activity into a task before and another task after the existing on
//first create the 2nd part of the task (after the existing one)
TechActivityModel activityContinued = new TechActivityModel()
{
Start = techActivity[i].End,
End = clash.End,
Subject = "++" + clash.Subject
};
activityContinued.Length = (int)(activityContinued.End - activityContinued.Start).TotalMinutes;
techActivity.Add(activityContinued);
//update the clashing task to finish when the existing task starts
clash.Subject = "+" + clash.Subject;
clash.End = techActivity[i].Start;
clash.Length = (int)(clash.End - clash.Start).TotalMinutes;
clash = techActivity.Where(t => t.Id != techActivity[i].Id && t.Start < techActivity[i].Start && t.End > techActivity[i].End).FirstOrDefault();
}
}
for (int i = techActivity.Count - 1; i >= 0; i--)
{
//something starts before and ends during
TechActivityModel clash = techActivity.Where(t => t.Id != techActivity[i].Id && t.Start <= techActivity[i].Start && t.End > techActivity[i].Start).FirstOrDefault();
while (clash != null)
{
//update the clashing task to finish when the existing task starts
clash.Subject = "/" + clash.Subject;
clash.End = techActivity[i].Start;
clash.Length = (int)(clash.End - clash.Start).TotalMinutes;
clash = techActivity.Where(t => t.Id != techActivity[i].Id && t.Start < techActivity[i].Start && t.End > techActivity[i].Start).FirstOrDefault();
}
}
techActivity = techActivity.OrderBy(t => t.Start).ToList();
//now we're going to pad all the gaps
List<TechActivityModel> newList = new List<TechActivityModel>();
DateTime LastEnd = techActivity[0].End;
//start with the gap from midnight till the first task
newList.Add(new TechActivityModel()
{
Start = new DateTime(2018, 4, 10),
End = techActivity[0].Start,
TicketNumber = 0,
Note = "",
TicketSubject = "",
TimeLogged = 0,
Id = 0
}
);
//pad all the gaps
for (int i = 1; i < techActivity.Count; i++)
{
if (LastEnd < techActivity[i].Start.AddMinutes(-2))
{
TechActivityModel gap = new TechActivityModel()
{
Start = LastEnd.AddMinutes(1),
End = techActivity[i].Start,
Subject = "",
Id = 0
};
gap.Length = (int)(gap.End - gap.Start).TotalMinutes;
newList.Add(gap);
}
LastEnd = techActivity[i].End;
}
//and finally fill the gap from the last task till midnight
newList.Add(new TechActivityModel()
{
Start = LastEnd,
End = new DateTime(2018, 4, 11),
Subject = "",
Length = 0
}
);
newList.AddRange(techActivity);
string content = "";
foreach (TechActivityModel techActivityModel in newList.OrderBy(t => t.Start))
{
content +=
techActivityModel.Start.ToShortTimeString()
+ " - " + techActivityModel.End.ToShortTimeString()
+ " (" + techActivityModel.Length + "mins)"
+ " : " + techActivityModel.Subject
+ Environment.NewLine;
}
Here's how I'd approach you problem, if I understand you correctly. I'd start by finding all of the distinct Start and End values, all together in a single list. We want them distinct and in order. Then we zip that list with itself to generate pairs:
var l = new List<Appointment>();
var ex = l.SelectMany(a => new[] { a.Start, a.End }).Distinct().OrderBy(dt => dt);
var pairs = ex.Zip(ex.Skip(1), (First, Second) => new { First, Second });
Now, using the rule I mentioned in the comments:
Two time periods overlap if period a starts before period b ends and period b starts before period a ends.
We work through each pair of datetimes and re-query our appointments list for all appointments that overlap the period described by the pair. If you get no results, this time period is currently free. If you get one result, you have (for this period) an appointment that's not overlapped by anything else. If you get multiple results, you have a clash and can work out how to display that. But note that you don't have to keep revisiting this time period and extending/shortening it, updating its text, etc.
Greetings Please keep in mind there is no database and these are fake functions to make the component work for testing, i have a List which makes a 24 hours based on 15 minutes scale and produces from this method:
public List<ScaleLine> GetHoursAndScales(ScaleLine.Scales scale = ScaleLine.Scales.Fiftheen)
{
int _scale = Convert.ToInt32(scale);
int _count = _scale * 24;
int _scaleCount = 60 / _scale;
List<ScaleLine> _list = new List<ScaleLine>();
var start = DateTime.Today;
var clockQuery = from offset in Enumerable.Range(1, _count)
select TimeSpan.FromMinutes(_scaleCount * offset);
foreach (var time in clockQuery)
{
_list.Add(new ScaleLine() { Id = _list.Count, Hours = (start + time).ToString("HH:mm"), Scale = _scale });
}
return _list;
}
And i have another list which is called Reserved hours which is produces on this method:
public List<Reservedhours> AllReservedHours()
{
return new List<Reservedhours>
{
new Reservedhours() { Id = 1, Date = DateTime.Now, StartPoint = "08:00", EndPoint = "10:00" },
new Reservedhours() { Id = 2, Date = DateTime.Now, StartPoint = "14:00", EndPoint = "16:00" },
new Reservedhours() { Id = 3, Date = DateTime.Now, StartPoint = "20:00", EndPoint = "22:00" },
new Reservedhours() { Id = 4, Date = DateTime.Now.AddDays(1), StartPoint = "07:00", EndPoint = "11:00" },
new Reservedhours() { Id = 5, Date = DateTime.Now.AddDays(1), StartPoint = "13:00", EndPoint = "15:00" },
new Reservedhours() { Id = 6, Date = DateTime.Now.AddDays(1), StartPoint = "15:00", EndPoint = "18:00" },
new Reservedhours() { Id = 7, Date = DateTime.Now.AddDays(1), StartPoint = "18:00", EndPoint = "22:00" },
};
}
Now i have another list that produces the available hours based on reserved hours and first list that produces 24 hours:
public List<ScaleLine> GetAvailableHours(DateTime date, ScaleLine.Scales scale = ScaleLine.Scales.Fiftheen)
{
List<Reservedhours> _timeLine = AllReservedHours().Where(x => x.Date.Date == date.Date)
.Select( a => new Reservedhours {StartPoint = a.StartPoint, EndPoint = a.EndPoint } )
.ToList();
List<ScaleLine> _available = GetHoursAndScales();
//scale convert:
int _scale = Convert.ToInt32(scale);
foreach (var _item in _timeLine)
{
int index = _available.Where(x => x.Hours == _item.StartPoint)
.SingleOrDefault().Id;
//Convert to datetime
DateTime _opening = DateTime.ParseExact(_item.StartPoint, "HH:mm", System.Globalization.CultureInfo.InvariantCulture);
DateTime _closing = DateTime.ParseExact(_item.EndPoint, "HH:mm", System.Globalization.CultureInfo.InvariantCulture);
//Getting duration time
TimeSpan duration = _closing.Subtract(_opening);
double _duration = duration.TotalMinutes;
//getting coverage
int timeScale = 60 / _scale;
int coverage = Convert.ToInt32(_duration) / timeScale;
//remove unavailable timespots
_available.RemoveRange(index, coverage);
}
return _available;
}
But problem is when the Foreach loop starts it removes the first range correctly based on its index, lets say if _available list has 96 members it removes 8 of them, so second time it should have 88 members and find the index within those 88 members but it doesn't and gets the index wrong (it takes the index as if the list still had 96 members) and so goes for every other action within the loop. how can i fix this issue? is there a way i can get the available list without doing a foreach loop?
The problem is your determination of the index. Instead of asking the list for the index of the desired object, you ask for the property value of an object and using this as index:
int index = _available.Where(x => x.Hours == _item.StartPoint)
.SingleOrDefault()
.Id;
Either you ask really for the index by calling IndexOf():
var matchingItem = _available.Where(x => x.Hours == _item.StartPoint)
.First();
var index = _available.IndexOf(matchingItem);
Or you replace your .RemoveRange() by something else, that really removes your desired elements.
I have an array that contains data as follows (string and time data separated by a comma):
array[0]= Channel 1, 01:05:36
array[1]= Channel 2, 02:25:36
array[2]= Group 1, 22:25:36
array[3]= Netwk, 41:40:09
array[4]= LossOf, 03:21:17
array[5]= LossOf, 01:13:28
array[6]= Channel 1, 04:25:36
array[7]= Channel 2, 00:25:36
.
.
.
array[xxx]= xxx, xxx
I would like to count all duplicates items and determine average time for each duplicate found as follows:
Item1, Channel 1, 2 occurrences, average time for each occurrence is about xx minutes
Item2, Channel 2, 2 occurrences, average time for each occurrence is about xx minutes
Item3, LossOf, 2 occurrences, average time for each occurrence is about xx minutes
The time format is hh:mm:ss
This is what I have done so far, which only gives me total times of duplicates:
public void CountDuplicates(string[] myStringArray)
{
//count duplicates
ArrayList list = new ArrayList();
int loopCnt=0;
foreach (string item in myStringArray)
{
if (!String.IsNullOrEmpty(myStringArray[loopCnt]) == true)
list.Add(item);
loopCnt++;
}
loopCnt = 0;
Dictionary<string, int> distinctItems = new Dictionary<string, int>();
foreach (string item in list)
{
if (!distinctItems.ContainsKey(item))
{
distinctItems.Add(item, 0);
loopCnt++;
}
distinctItems[item] += 1;
}
foreach (KeyValuePair<string, int> distinctItem in distinctItems)
{
txtDisplayResults.AppendText("Alarm Error: " + distinctItem.Key + ", How many times: " + distinctItem.Value + "\r\n");
}
}
Maybe this:
a channel class to hold your values
class Channel
{
public String Name { get; set; }
public TimeSpan Duration { get; set; }
}
your sample data
var array = new[]{
"Channel 1, 01:05:36",
"Channel 2, 02:25:36",
"Group 1, 22:25:36",
"Network, 41:40:09",
"Loss of, 03:21:17",
"Loss of, 01:13:28",
"Channel 1, 04:25:36",
"Channel 2, 00:25:36",
};
the query
var channelGroups = array.Select(s =>
{
var tokens = s.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
var tsTokens = tokens[1].Split(':');
return new Channel()
{
Name = tokens[0],
Duration = new TimeSpan(
int.Parse(tsTokens[0]), // hours
int.Parse(tsTokens[1]), // minutes
int.Parse(tsTokens[2])) // seconds
};
})
.GroupBy(c => c.Name)
.Select(g => new
{
Channel = g.Key,
Count = g.Count(),
Average = g.Average(c => c.Duration.TotalMinutes)
});
output:
foreach(var group in channelGroups)
{
Console.WriteLine("Channel:[{0}] Count:[{1}] Average:[{2}]"
, group.Channel, group.Count, group.Average);
}
demo: http://ideone.com/6dF6s
Channel:[Channel 1] Count:[2] Average:[165.6]
Channel:[Channel 2] Count:[2] Average:[85.6]
Channel:[Group 1] Count:[1] Average:[1345.6]
Channel:[Network] Count:[1] Average:[2500.15]
Channel:[Loss of] Count:[2] Average:[137.375]
something like to following help?
var regex=new Regex(#"^(?<item>.*), (?<hours>\d{2}):(?<minutes>\d{2}):(?<seconds>\d{2})$");
var result=array.Select(a=> regex.Match(a)).Where(a=>a.Success)
.Select (a => new {
item=a.Groups["item"].Value,
time=decimal.Parse(a.Groups["hours"].Value)*60 +
decimal.Parse(a.Groups["minutes"].Value) +
decimal.Parse(a.Groups["seconds"].Value)/60
})
.GroupBy (a => a.item)
.Select (a =>new {item=a.Key, duplicates=a.Count(),time=a.Average (b => b.time)} );
to format as a string you can then do:
var resultString=result.Aggregate(new StringBuilder(),(sb,a)=>sb.AppendFormat("Item: {0}, # of occurences: {1}, Average Time: {2:0.00}\r\n",a.item,a.duplicates,a.time)).ToString();
resultString will now be the string you are looking for.
EDIT -- After request to use a dictionary, as tuple is unavailable you need to define a class to hold the temporary data. I've used pulbic fields but you could easily expand to use properties round private fields
public class Data {
public int Occurrences;
public decimal Time;
public Data(int occurrences, decimal time) {
this.Occurrences=occurrences;
this.Time=time;
}
}
var regex=new Regex(#"^(?<item>.*), (?<hours>\d{2}):(?<minutes>\d{2}):(?<seconds>\d{2})$");
var dict = new Dictionary<string,Data>();
foreach (var entry in array) {
if (regex.IsMatch(entry)) {
var match=regex.Match(entry);
var item=match.Groups["item"].Value;
var time=decimal.Parse(match.Groups["hours"].Value)*60 +
decimal.Parse(match.Groups["minutes"].Value) +
decimal.Parse(match.Groups["seconds"].Value)/60;
if (dict.ContainsKey(item)) {
dict[item].Occurrences++;
dict[item].Time+=time);
} else {
dict[item]=new Data(1,time);
}
}
}
StringBuilder sb=new StringBuilder();
foreach (var key in dict.Keys) {
sb.AppendFormat("Item: {0}, # of occurences: {1}, Average Time: {2:0.00}\r\n", key, dict[key].Occurrences, dict[key].Time / dict[key].Occurrences);
}
var resultString=sb.ToString();