C# start with certain value in foreach - c#

Is there a way to start with a certain value in the foreach? In specific I would like to start with 7:30AM.
DateTime start = DateTime.Today;
var clock = from offset in Enumerable.Range(0, 48)
select start.AddMinutes(30 * offset);
foreach (DateTime time in clock)
{
string timeValue = time.ToString("hh:mmtt");
}

In short, no. Just make your Enumerable.Range cover the right range (might want to double-check):
var clock = from offset in Enumerable.Range(15, 48)
select start.AddMinutes(30 * offset);
Edit
A cleaner solution might be something like:
DateTime start = // set start
DateTime end = // set end
while (start <= end) {
// do stuff here
start = start.AddMinutes(30);
}

It is not possible to "start" at a different value in the collection with the foreach syntax directly (wrapping is an option and the following example could even be considered a form of wrapping -- LINQ generation is often lazy), but it is possible to iterate over an appropriately modified collection and obtain the desired semantics.
Enumerable.Range returns something IEnumerable<T> -- you can always Where-limit or Skip-through it (e.g., see the IEnumerable/LINQ (extension) methods) and then use the resultant as the "for each" collection.
For instance (this relies on the fact that time is increasing and somewhat "inefficient", but chances of it even mattering are close to nil in most applications):
foreach (DateTime time in clock.Where(t => t >= someStartTime)) {
string timeValue = time.ToString("hh:mmtt");
}
Another solution based on ide's comment (which is, in my mind, cleaner than the above -- it also has to evaluate the conditional less):
foreach (DateTime time in clock.SkipWhile(t => t < someStartTime)) {
...
}
Note that these IEnumerable results are lazily evaluated streams.
However, some of the other answers provide nice alternative solutions and different ways of approaching the problem. I would only use the above if the initial sequence could not be altered and/or was used for different purposes requiring the original data.
Happy coding.

You can set your start to 7:30AM today, like this:
DateTime start = new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, 7, 30, 0);

Just replace this, It'll start with 7:30 AM.
var clock = from offset in Enumerable.Range(15, 48)
Good luck!

DateTime start = DateTime.Today.Add(new TimeSpan(7, 30, 0));
var clock = from offset in Enumerable.Range(0, 48)
select start.AddMinutes(30 * offset);
foreach (DateTime time in clock)
{
string timeValue = time.ToString("hh:mmtt");
}
So, to start from 7:30 today, the key line is the first one:
DateTime start = DateTime.Today.Add(new TimeSpan(7, 30, 0));

It would be clearest is you set the time specifically and add to that. By simply changing the starting point of the enumeration, you'll get the desired starting times but it is unclear what the value is by inspection.
var start = DateTime.Today.Add(new TimeSpan(7, 30, 0));
var times = Enumerable.Range(0, 48)
.Select(offset => start.AddMinutes(30 * offset));
foreach (var time in times)
{
string timeValue = time.ToString("hh:mmtt");
// ...
}

Related

Find most recent time from list compared to my current time C#

I have a list of times:
1:00
3:00
5:00
I need to select the most recent one compared to my current time.
So say the current time is 2:50, I need the code to select 1:00.
If the current time was 3:30, it would select 3:00, etc.
How can I write a for loop to accomplish this with any given list of times and a current time?
You can query times with a help of Linq MinBy, e.g. having
TimeOnly[] times = {
new TimeOnly(1, 0),
new TimeOnly(3, 0),
new TimeOnly(5, 0),
};
and
TimeOnly current = new TimeOnly(2, 50);
you can put
using System.Linq;
...
var mostRecent = times
.Where(time => time <= current) // past time only
.MinBy(time => current - time);
No Linq solution can be a for loop:
TimeOnly mostRecent = TimeOnly.MinValue;
foreach (TimeOnly time in times)
if (time <= current && mostRecent < time)
mostRecent = time;
Edit: If collection (TimeOnly[] times) is in fact ordered we can use binary search instead of scan (see Eric J. comment):
// if times is List<> then the syntax should be
// int index = times.BinarySearch(current);
int index = Array.BinarySearch(times, current);
TimeOnly mostRecent = index >= 0 ? times[index] : times[~index - 1];

In C#, what is the best way to Parse out and sort by time string?

I am reading and loading in files into Excel using C# VSTO and the filenames are something like this:
C:\myfiles\1000AM.csv
C:\myfiles\1100AM.csv
C:\myfiles\1200PM.csv
C:\myfiles\100PM.csv
C:\myfiles\200PM.csv
And then i am putting these in a list and need to sort these by "time".
How can i convert the string in the format above into a time object that i can use to sort on?
You need extract the time parts somehow and then compare them to each other.
You could for example do this using a Comparison<string>. Here is an example that uses the Span<T> type to do this without allocating any additional garbage:
List<string> list = new List<string>() { ... }
list.Sort((a, b) =>
{
//compare AM/PM
int compareAmAndPm = a.AsSpan().Slice(a.Length - 6, 2)
.CompareTo(b.AsSpan().Slice(b.Length - 6, 2), StringComparison.Ordinal);
if (compareAmAndPm != 0)
return compareAmAndPm;
//compare the times as integers
int index = a.LastIndexOf('\\');
var firstTime = int.Parse(a.AsSpan().Slice(index + 1, a.Length - index - 7));
index = b.LastIndexOf('\\');
var secondTime = int.Parse(b.AsSpan().Slice(index + 1, b.Length - index - 7));
return firstTime.CompareTo(secondTime);
});
It should give you a result of this:
C:\myfiles\1000AM.csv
C:\myfiles\1100AM.csv
C:\myfiles\100PM.csv
C:\myfiles\200PM.csv
C:\myfiles\1200PM.csv
From practice we figured out, that a Time or Date on it's own does not work 99% of the cases. We need both plus the timezone to have any hope of processing them meaningfully.
That is why we only have things like DateTime, nowadays. Ideally those file names should consist of the full DateTime, in UTC and Invariant culture. If you got the option to change how those are created, use it.
However if you consistently only have one part that is not an issue: DateTime simply used default values for the other two. And as those two will be consistent, they will work. The only issue will be a finding a culture setting that eats that AM/PM format.

In C#, What is the best way to aggregate and transform one DateTime collection into another?

I have a little calendar tool in C# and I am trying to figure out how to do a conversion from one array of DateTime objects to another. Here are the details:
I start off with collection of DateTime object
IEnumerable<DateTime> slots = GetSlots();
where each DateTime represents that starting time of an available slot (think open slot in calendar) All slots are for 30 minutes This is a given. So for example:
var slots = new List<DateTime>()
slots.Add(DateTime.Today + new TimeSpan(5,00, 0));
slots.Add(DateTime.Today + new TimeSpan(9,00, 0));
slots.Add(DateTime.Today + new TimeSpan(9,30, 0));
slots.Add(DateTime.Today + new TimeSpan(10,00, 0));
slots.Add(DateTime.Today + new TimeSpan(10,30, 0));
slots.Add(DateTime.Today + new TimeSpan(11,00, 0));
slots.Add(DateTime.Today + new TimeSpan(16,30, 0));
in the above example, it means i am free:
From 5:00 - 5:30
From 9:00 - 9:30
From 9:30 - 10:00
From 10:00 - 10:30
From 10:30 - 11:00
From 11:00 - 11:30
From 4:30 - 5:00
because i take the time from the item in the collection as the start time and simply add 30 minutes to it and that is considered a free slot.
I now have the requirement to take a larger time window (lets use 2 hours) and find out how many 2 hour slots free i have so I now need to take this array of dates and "merge" into into bigger buckets. Given the bigger bucket is 2 hours (120 minutes), I want a function like this
IEnumerable<DateTime> aggregateArray = MergeIntoLargerSlots(slots, 120);
I would basically have to loop through the slots array above and "merge" items that are lined up next to each out to make bigger buckets. If any of the merged items is 2 hours long then that should show up as an entry in the resulting array. Using the example above the resulting aggregateArray would have 2 items in the collection it that would have the times:
9AM (because i have a free slot from 9-11 AM (120 mins).
9:30AM (because i have a free slot from 9:30-11:30 AM (120 mins).
NOTE: 30 minutes "chunks" are the smallest interval so DON'T need to include 9:05 to 11:05 as an example
So given the previous array I have two 2 hour windows of time free in the day
I am struggling to figure out how this MergeIntoLargerSlots function would work so i would hoping to get some suggestion for how to approach this problem.
This only works for half hour intervals, you can figure out to make it work for others if you need to.
public List<DateTime> MergeIntoLargerSlots(List<DateTime> slots, int minutes)
{
int count = minutes/30;
List<DateTime> retVal = new List<DateTime>();
foreach (DateTime slot in slots)
{
DateTime end = slot.AddMinutes(minutes);
if (slots.Where(x => x >= slot && x < end).Count() == count)
{
retVal.Add(slot);
}
}
return retVal;
}
Here's a brief explanation of my problem solving approach; I take in the minutes and the slots list. I add minutes to get an end time which gives me range. From there, I use the Where operator to produce and IEnumerable<DateTime> from slots that has the slots in that range. I compare the result to the count variable I got from doing minutes/slotLength if the numbers match then you have the necessary slots. With your sample data the result of the Where for 9 AM would have 4 values in it; 9, 9:30, 10 and 10:30, ofc the count is 4, 120/30 == 4, so that gets added to retVal. The same would be true for 9:30, no other times would be returned.
Evan beat me to and did it with one less loop, but here was my solution:
private List<DateTime> MergeArray(List<DateTime> slots, int minutes)
{
var segments = minutes / InitialSegment;
var validSegments = new List<DateTime>();
foreach (var slot in slots.OrderBy(x => x))
{
var validSegment = true;
for (var i = 0; i < segments-1; i++)
{
var next = slot.AddMinutes(InitialSegment * (i + 1));
if (slots.All(x => x != next))
{
validSegment = false;
break;
}
}
if (validSegment)
validSegments.Add(slot);
}
return validSegments;
}
Assuming that your original list is sorted (if it is not, make it so it is), you could loop through your original list and check whether adjacent items are consecutive (i.e. whether the start times have a distance of exactly 30 minutes). Always keep track of the first item in the current series of consecutive timeslots - once you reach four of them (with 4 consecutive 30 minutes timeslots adding up to a possible two-hour timeslot; other timeslot sizes obviously require different factors), save a new two-hour timeslot into your resulting list and update your reference to the beginning of the current series of consecutive items.
Untested, so please consider this as pseudocode:
var twoHourSlots = new List<DateTime>();
int consecutiveSlotsCount = 0;
DateTime? previousSlot;
foreach (DateTime smallSlotStart in slots) {
if (previousSlot.HasValue) {
if (smallSlotStart - previousSlot.Value == new TimeSpan(0, 30, 0)) {
consecutiveSlotsCount++;
} else {
consecutiveSlotsCount = 0;
}
}
if (consecutiveSlotsCount == 4) {
twoHourSlots.Add(smallSlotStart - new TimeSpan(1, 30, 0));
consecutiveSlots = 0;
previousSlot = null;
} else {
previousSlot = smallSlotStart;
}
}
Some things to note:
I am using arithmetic operators on DateTime values. Check the docs to find out more; they do handy things and often let you work with TimeSpan values automatically.
I am using a TimeSpan constructor that takes hours, minutes and seconds several times. So that's what the three numbers mean.
I have declared previousSlot, a variable that keeps track of the last slot looked at (to compare to the current one), as DateTime? (again, check the docs if you are not sure what a nullable type is). That is because in the first iteration of the foreach loop, there is no previous slot to look at and the loop has to behave differently.
Likewise, previousSlot is set to null when we have found a 2-hour slot, as the last 30-minute slot of the found 2-hour slot should not be counted to the next possible 2-hour slot.
Once four consecutive 30-minute slots have been found, one hour and thirty minutes are subtracted from the beginning of the last one. That is because the thirty minutes after the beginning of the last 30-minute slot will be part of the resulting 2-hour slot.
I would create a TimeInterval class since there are a lot of other interesting things you can do with it.
public sealed class TimeInterval
{
public DateTime Start { get; private set; }
public DateTime End { get { return Start.AddMinutes(Duration); } }
public double Duration { get; private set; }
public TimeInterval(DateTime start, int duration)
{
Start = start;
Duration = duration;
}
public IEnumerable<TimeInterval> Merge(TimeInterval that)
{
if(that.Start >= this.Start && that.Start <= this.End)
{
if(that.End > this.End)
Duration += (that.Duration - (this.End - that.Start).TotalMinutes);
yield return this;
}
else
{
yield return this;
yield return that;
}
}
}
And this is an O(n) merge algorithm that will work for intervals of arbitrary sizes (in minutes).
//the `spans` parameter must be presorted
public IEnumerable<TimeInterval> Merge(IEnumerable<TimeInterval> spans, int duration)
{
var stack = new Stack<TimeInterval>();
stack.Push(spans.First());
foreach (var span in spans.Skip(1))
foreach(var interval in stack.Pop().Merge(span)) //this enumeration is guaranteed to have either one element or two elements.
stack.Push(interval);
return from interval in stack where interval.Duration >= duration select interval;
}

Recursion Help required with C#

On our windows application, We have startDate and EndDate. On click of Execute button event, we need to call a third party web service with our search string + daterange( date from 01/01/2010 to 12/31/2010). Now our search criteria can return us thousands of records but web service have limitation of able to return only 10K records per transaction.
Which required us to break down our dateRange. So basically we need following;
For (X dateRange if RecordCount > 10000) then
X dateRange/2 which will be 01/01/2010 to 06/01/2010 in our case and check condition again and do this recursively until we get daterange block where RecordCount is < 10000.
Then start with Next date, for example, if we get 9999 records for 01/01/2010 to 03/30/2010 then we need to get records for next block starting 04/01/2010
Is this possible with Recursion?
RecursionFunction(dtStart, dtEnd)
{
if (WebService.RecordCount > 9999)
{
TimeSpan timeSpan = dtEnd.Subtract(dtStart);
DateTime mStart = dtStart;
DateTime mEnd = dtStart.AddDays(timeSpan.Days / 2);
RecursionFunction(dtStart,dtEnd);
}
else
{
Get Records here
}
}
But with above code, recursion will have following blocks
01/01/2010, 12/31/2010 > 10000
01/01/2010, 07/03/2010 > 10000
01/01/2010, 04/02/2010 < 10000
So after finishing getting record, recursion will start again with block 01/01/2010,07/03/2010 which we don't need. We need to start next recursion with 04/03/2010,12/31/2010
Thanks in advance for help.
It looks like you are trying to split the input range until it is small enough to handle. Try calling it for both ranges:
RecursionFunction(mStart, mEnd);
RecursionFunction(mEnd.AddDays(1), dtEnd);
The first step is to change the RecursionFunction call (at line 8 of your example) to:
RecursionFunction(mStart, mEnd);
But, then, you'll also need to call it again with the other half of the date range.
RecursionFunction(mEnd + AddDays(1), dtEnd);
Also, you need to handle the results (presumably combining the two answers).
var set1 = RecurseFunction(...);
var set2 = RecurseFunction(...);
return set1.Concat(set2);
This is like divide and conquer. You need to get results from the left and the right of the split and combine them and return that value. So you can keep getting smaller until you have enough data you can deal with and just return that. Then keep joining the result sets together.
public IList<Data> GetRecords(DateTime start, DateTime end)
{
var RecordCount = WebService.RecordCount(start, end);
if (RecordCount < 10000) return WebService.GetRecords(start, end);
DateTime l, m, e;
l = start;
e = end;
var midDay = end.Subtract(start).TotalDays / 2;
m = start.AddDays(midDay);
var left = GetRecords(l, m);
var right = GetRecords(m.AddDays(1), e);
return left.Concat(right);
}
This is how I would do it
static List<string> RecursiveGet(DateTime StartDate, DateTime EndDate, List<string> Output)
{
if (Webservice.RecordCount > 9999)
{
TimeSpan T = EndDate.Subtract(StartDate);
T = new TimeSpan((long)(T.Ticks / 2));
DateTime MidDate = StartDate.Add(T);
Output.AddRange(RecursiveGet(StartDate, MidDate, Output));
Output.AddRange(RecursiveGet(MidDate.AddMilliseconds(1), EndDate, Output));
}
else
{
//Get Records here, return them in array
Output.Add("Test");
}
return Output;
}
static List<string> GetRecords(DateTime StartDate, DateTime EndDate)
{
return RecursiveGet (StartDate, EndDate, new List<string>());
}
Note, Couldn't test it
It works by dividing the dates in half, then searching each of them, and if one is still bigger than 9999, then doing it again.
An easy way would be a form of pagination. If your using JSON or XML, you can put the amount of total results and just return a set number of results (return the offset too). This way you can do a loop to check if your on the last page and after you get the last results page, break out of it.
Don't forget to put checks in if a particular transaction fails though. It's not an ideal solution on such a large dataset but it is a workaround
It sounds much easier to just reuse the last date for the data you actually got back in a while-loop than to home in with recursion like this.
Then start with Next date, for example, if we get 9999 records for 01/01/2010 to 03/30/2010 then we need to get records for next block starting 04/01/2010
March has 31 days.
Pseudo-C# code
var dtStart = DateTime.Parse("2010-01-01");
var dtEnd = DateTime.Parse("2010-12-31");
var totalRecords = new List<RecordType>();
var records = WebService.Get(dtStart, dtEnd);
totalRecords.Add(records);
while (dtStart < dtEnd && records.Count > 9999)
{
dtStart=records.Last().Date;
records = WebService.Get(dtStart, dtEnd);
totalRecords.Add(records);
}
To ease the load on the service you could calculate the timespan for the previous run and only get that many days for the next run in the while-loop.
How you should handle the inevitable doublets depends on the data on the records.
I just realized I presumed you had a date in the returned data. If not, then disregard this answer.

please help optimize this loop

i need to calculate the number of workdays between two dates. a workday is any day between Monday through Friday except for holidays. the code below does this, but it uses a loop. does anyone see a way to get rid of the loop or at least optimize it?
thanks
konstantin
using System;
using System.Linq;
namespace consapp
{
static class Program
{
static void Main(string[] args)
{
var holidays = new DateTime[] { new DateTime(2010, 11, 23), new DateTime(2010, 11, 30) };
var date_start = new DateTime(2010, 12, 3);
var date_end = date_start.AddDays(-9.9);
var duration = (date_end - date_start).Duration();
for (var d = date_end; d < date_start; d = d.Date.AddDays(1))
{
if (holidays.Contains(d.Date) || d.DayOfWeek == DayOfWeek.Saturday || d.DayOfWeek == DayOfWeek.Sunday)
{
duration -= TimeSpan.FromDays(1) - d.TimeOfDay;
}
}
Console.WriteLine(duration);
}
}
}
I would investigate an algorithm like the following. (Sorry no code provided.)
Count the number of full 7-day weeks between your start and end date. You should be able to do this with .NET DateTime and TimeSpan objects.
For each full 7-day week, add 5 days to your result.
Figure out the partial weeks including your start and end dates.
Loop through your holidays and reduce your result by 1 for each holiday between your start and end date. Looping is better here because there are likely far fewer holidays to loop over than days between your start and end date.
Have fun!
EDIT: For source code, check out this answer: Calculate the number of business days between two dates?
Is performance really problematic here? Unless profiling suggests otherwise I'd guess this code doesn't really slow down your application. It's performance should be fine unless you calculate the workdays for thousands of long intervals per second.
If your holiday list is much larger than just two dates then convert it into a HashSet<T> which has O(1) lookup time.
And of course you can turn around the code. So you don't loop over the days in the interval, but over the holidays. Then you just calculate the number of week-days in the interval(should be simple math) and subtract the number of holidays that fall on a week-day.
If it's really necessary you can pre-calculate the workdays since some fixed date, and then subtract the lookup result from the beginning of the period from the lookup result from the end of the period.
if you want faster code, don't loop over each day in the range:
remove from your list of holidays all holidays that fall on sunday or saturday, then use the timespan methods to give you the number of days between the two dates. With a little math (think about integer division by 7) you can get the number of mon-thursday days in that range, subtract the number of holidays that don't fall on the weekend from that number and you are done.
Just roll with it as is. This is not going to waste much time since the bounds are small. When you have some working code, move on. No need to mercilessly optimise code for no reason.
just because I started this as a fun puzzle, here's the code:
[Test]
public void TestDateTime() {
var start = DateTime.Now.Date;
var end = DateTime.Now.Date.AddDays(35);
var workdays = (end - start).Days - ((end - start).Days/7)*2
- (((end - start).Days%7==0)?0:(((int)start.DayOfWeek==0)?1:Math.Max(Math.Min((int)start.DayOfWeek + (end - start).Days%7 - 6, 2), 0)));
new []{DateTime.Now.AddDays(19), DateTime.Now.AddDays(20)}.ToList().ForEach(
x => { if (x.DayOfWeek != DayOfWeek.Saturday && x.DayOfWeek != DayOfWeek.Sunday) workdays--; });
Console.Out.WriteLine("workdays = {0}", workdays);
}
Christmas day and Boxing day are included as holidays.

Categories

Resources