I have a requirement to have a relative min/max date validation able to be stored in a database to customize an application per customer. I decided that the NodaTime.Period due to it's capability to specify years was the best choice. However, NodaTime.Period does not offer a way to compare itself against another period.
Example data provided for this validation:
Minimum Age of 18 years old.
Maximum Age o 100 years old.
Minimum sale duration of 1 month
Maximum sale duration of 3 months
Minimum advertising campaign 7 days
(Note: Current requirements are that Year / Month / Day will not be combined in validations)
The validations are:
public Period RelativeMinimum { get; set; }
public Period RelativeMaximum { get; set; }
Given a user entered date (and now):
var now = new LocalDate(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day);
var userValue = new LocalDate(date.Year, date.Month, date.Day);
var difference = Period.Between(userValue, now);
I have a comparison of:
if(RelativeMinimum != null && difference.IsLessThan(RelativeMinimum))))
{
response.IsValid = false;
response.Errors.Add(MinimumErrorMessage);
}
Which is consuming an extensions class:
public static class PeriodExtensions
{
public static bool IsLessThan(this Period p, Period p2)
{
return (p.Years < p2.Years) || (p.Years == p2.Years && p.Months < p2.Months) || (p.Years == p2.Years && p.Months == p2.Months && p.Days < p2.Days);
}
public static bool IsGreaterThan(this Period p, Period p2)
{
return (p.Years > p2.Years) || (p.Years == p2.Years && p.Months > p2.Months) || (p.Years == p2.Years && p.Months == p2.Months && p.Days > p2.Days);
}
}
While this approach works, given the test conditions I have, I have to wonder why #jon-skeet didn't implement this, and immediately have to worry over what am I missing and what alternative should I be using instead?
The main reason periods aren't comparable is that they can contain components of variable lengths.
Two one-month periods aren't necessarily the same number of days long. As an example, which is greater: 1 month or 30 days? If the month is January, then that's longer than 30 days. If the month is February, that's less than 30 days.
The same applies to years. Some are 365 days long, some are 366.
Of course, that all assumes you're using the Gregorian calendar. Noda Time supports other calendar systems, and there are similar quirks in those as well.
Regarding the code:
If you want a LocalDate from a DateTime, use LocalDateTime.FromDateTime(dt).Date
To get the current date, use SystemClock.Instance.Now.InZone(tz).Date
If you intended that to be the same as DateTime.Now, which uses the local time zone of the computer where the code is running, then get tz by calling DateTimeZoneProviders.Tzdb.GetSystemDefault()
For comparison of the type of problem you have described, consider defining min and max days instead of min and max periods. Then you wont have such variation of units. You can get the difference in days like this:
long days = Period.Between(d1, d2, PeriodUnits.Days).Days;
I believe something like this would work well for your use case:
public static bool IsDifferenceLessThan(LocalDate d1, LocalDate d2, Period p)
{
if (p.HasTimeComponent)
throw new ArgumentException("Can only compare dates.", "p");
if (p.Years != 0)
{
if (p.Months != 0 || p.Weeks != 0 || p.Days != 0)
throw new ArgumentException("Can only compare one component of a period.", "p");
var years = Period.Between(d1, d2, PeriodUnits.Years).Years;
return years < p.Years;
}
if (p.Months != 0)
{
if (p.Weeks != 0 || p.Days != 0)
throw new ArgumentException("Can only compare one component of a period.", "p");
var months = Period.Between(d1, d2, PeriodUnits.Months).Months;
return months < p.Months;
}
if (p.Weeks != 0)
{
if (p.Days != 0)
throw new ArgumentException("Can only compare one component of a period.", "p");
var weeks = Period.Between(d1, d2, PeriodUnits.Weeks).Weeks;
return weeks < p.Weeks;
}
var days = Period.Between(d1, d2, PeriodUnits.Days).Days;
return days < p.Days;
}
Just as an additional point to Matt's already-excellent answer, we provide an option for creating an IComparer<Period> with a specific anchor point, e.g.
var febComparer = Period.CreateComparer(new LocalDate(2015, 2, 1).AtMidnight());
var marchComparer = Period.CreateComparer(new LocalDate(2015, 3, 1).AtMidnight());
var oneMonth = Period.FromMonths(1);
var twentyNineDays = Period.FromDays(29);
// -1: Feb 1st + 1 month is earlier than Feb 1st + 29 days
Console.WriteLine(febComparer.Compare(oneMonth, twentyNineDays));
// 1: March 1st + 1 month is later than March 1st + 29 days
Console.WriteLine(marchComparer.Compare(oneMonth, twentyNineDays));
Related
I have below class
public class DateGenerator
{
public DateOnly GeneratedDate {get; set;}
}
and list of dateGenerators List<DateGenerator> dateGenerators; as below
I need to verify whether all months are present for a year within the given range. For example, I have given a listed range; I need to identify the years first, and then for that year, whether all months are listed or not.
Here I have the same years along with the months listed, so the above condition also applies for the same years.
For example, not all months are listed below for the year 2009 and I have duplicate data for 2011. All months are not listed in the second set of the same data year, 2011, so I need to identify this.
Date (Month/Day/Year)
7/1/2009
8/1/2009
9/1/2009
10/1/2009
11/1/2009
12/1/2009
1/1/2010
2/1/2010
3/1/2010
4/1/2010
5/1/2010
6/1/2010
7/1/2010
8/1/2010
9/1/2010
10/1/2010
11/1/2010
12/1/2010
1/1/2011
2/1/2011
3/1/2011
4/1/2011
5/1/2011
6/1/2011
7/1/2011
8/1/2011
9/1/2011
10/1/2011
11/1/2011
12/1/2011
4/1/2011
5/1/2011
6/1/2011
7/1/2011
8/1/2011
9/1/2011
I can group by GeneratedDate and extract the year and compare, but I am unsure how to tackle the duplicate data. Could anyone please let me know how to approach the same to identify
Thanks in advance!!
Sample method:
public bool verifyCompleteMonthlySet(List<DateGenerator> dateGenerators)
{
//Identify whether all months are present for a year in a
//given a list, and also identify if there is
//any duplicate set found for a year, and all months are present for that same year as well
}
With LINQ you can write:
bool allMonthsAreListed = dateGenerators
.GroupBy(dg => dg.GeneratedDate.Year)
.All(g => g.Count() % 12 == 0);
Explanation:
We group by year.
For each group get the month count.
If the month count is divisible by 12 (with no remainder), all the sets in this year are complete.
Note that this is not perfect, e.g. there could be two incomplete sets in the same year whose month count adds up to 12. We would need to have a way to identify the sets. Do you have a set id or the like?
Also, I am not sure what you mean by "I need to identify this".
Do only need to know that some years or sets are not complete?
You you need to identify the years/sets?
Do you need to know the missing months?
Since we do not have a way to identify sets, things get a bit more complicated:
bool allMonthsAreListed = dateGenerators
.Select(dg => dg.GeneratedDate)
.GroupBy(d => d.Year)
.All(
g => g.Count() % 12 == 0 &&
g.GroupBy(d => d.Month)
.Select(g2 => g2.Count())
.Distinct()
.Count() == 1
);
Explanation:
We extract the date from the DateGenerators.
We group by year.
We test whether the count of months per year is divisible by 12 and ...
... that all months occur the same number of times. To do this, we:
group by month
get the count of each month group
get the distinct number of counts for all the groups
test if this number is 1.
Note that if we have one full set, each month occurs once, if we have two full sets, each month occurs twice, etc. If the first set is complete and the second set has only 11 months, the we have 11 times a count of 2 and one time a count of 1. I.e., the distinct number of counts is 2. This identifies an incomplete set.
Tested with
List<DateGenerator> dateGenerators = new();
// Year 2010 with one complete set
for (int i = 1; i <= 12; i++) {
dateGenerators.Add(new() { GeneratedDate = new DateOnly(2010, i, 1) });
}
// Year 2011 with 12 months but from two incomplete sets
for (int i = 1; i <= 10; i++) { // Set 1: months 1 to 10
dateGenerators.Add(new() { GeneratedDate = new DateOnly(2011, i, 1) });
}
for (int i = 5; i <= 6; i++) { // Set 2: months 5 and 6
dateGenerators.Add(new() { GeneratedDate = new DateOnly(2011, i, 1) });
}
Also using System.Linq.GroupBy but then doing a forensic by removing from all.
static void Main(string[] args)
{
Console.Title = "Missing months";
// Get an array of DateTime.
var dates = inputData.Trim().Split(',').Select(_=>DateTime.Parse(_)).ToArray();
foreach (var year in dates.GroupBy(_=>_.Year))
{
Console.WriteLine($"{year.Key}");
var months = year.Select(_ => _.Month).Distinct().ToArray();
if (months.Length == 12)
{
Console.WriteLine($"All months present.");
}
else
{
var all = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 };
foreach (var month in months)
{
all.Remove(month);
}
Console.WriteLine($"{year.Key} missing {string.Join(", ", all )}");
}
foreach (var dup in year.GroupBy(_ => _).Where(_ => _.Count() > 1))
{
Console.WriteLine($"Duplicate dates: {dup.Key.ToShortDateString()} ({dup.Count()})");
}
Console.WriteLine();
}
Console.ReadKey();
}
Where inputData uses the data set from your post:
const string inputData = #"
7/1/2009,
8/1/2009,
9/1/2009,
10/1/2009,
11/1/2009,
12/1/2009,
1/1/2010,
2/1/2010,
3/1/2010,
4/1/2010,
5/1/2010,
6/1/2010,
7/1/2010,
8/1/2010,
9/1/2010,
10/1/2010,
11/1/2010,
12/1/2010,
1/1/2011,
2/1/2011,
3/1/2011,
4/1/2011,
5/1/2011,
6/1/2011,
7/1/2011,
8/1/2011,
9/1/2011,
10/1/2011,
11/1/2011,
12/1/2011,
4/1/2011,
5/1/2011,
6/1/2011,
7/1/2011,
8/1/2011,
9/1/2011";
I have a function that takes three parameters (day, month & year) and creates a new dateTime. It's a public function that gets called when any of three comboboxes are dropped down.
When unit testing I accidentally entered an invalid value and it threw a invalid date time exception, but this won't be possible in the application as the comboboxes are pre-populated with valid values only.
So question is should I still check and handle this exception in the function?
In general, yes, any public function could be called from anywhere and it is good practice to defend your code from invalid inputs also if, at certain point in time, you are sure about who feeds the inputs to the function.
However, this supposed function could handle the impossible situations by itself without triggering an exception if the inputs are not good.
It is relatively easy to check the inputs and follow the well known pattern of TryParse
public bool TryMakeDateTime(int year, int month, int day, out DateTime date)
{
date = DateTime.MinValue;
if(!IsValidDay(year, month, day))
return false;
date = new DateTime(year, month, day);
return true;
}
public bool IsValidDay(int year, int month, int day)
{
if(day < 1 || day > 31)
return false;
if(month < 1 || month > 12)
return false;
if(day > 30 && (month == 2 ||
month == 4 ||
month == 6 ||
month == 9 ||
month == 11))
return false;
// This is arbitrary, adjust the check to your constraints
if(year < 1900 || year > 2099)
return false;
if(month == 2)
{
// IsLeapYear cannot handle values below 1 or higher than 9999
// but we have already checked the year with more retrictive
// constraints.
int extraDay = (DateTime.IsLeapYear(year) ? 1 : 0);
if(day > (28 + extraDay))
return false;
}
return true;
}
Yes, the function should throw an exception for invalid inputs, if the function, in isolation, allows invalid inputs to be submitted. You don't know how, or from where, a future developer might call this function. But another, better option is to code the function so that only valid inputs are allowed.
You can do this by changing the type of the inputs from integer values to Enums. Create a Month Enum
public enum CalendarMonth {
NotSet = 0, January = 1, February = 2,
March = 3, April = 4, May = 5, June = 6,
July = 7, August = 8, September = 9,
October = 10, November = 11, December = 12}
and a DayOfMonth Enum
public enum DayOfMonth {
NotSet = 0, dom1 = 1, dom2 = 2, ....etc., ... dom31 = 31 }
You could code the function to treat the 31st of months with only 30 days in them as the first of the next month, and Feb 29, 30 and 31 as March 1,2,3, etc., to avoid treating this as invalid. Then your function's signature would be
public DateTime NewDate(DayOfMonth dom, CalendarMonth month, int year);
and it would not be possible to pass it invalid values. (except I guess for year values outside the DateIme.MinDate to DateTime.MaxDate range)
You should not prevent or catch the exception. But you should make sure an exception will really happen in that "impossible" case.
Exceptions are meant for "impossible" situations that are thought not to occur "normally".
If for example you call a DateTime constructor overload, that constructor will already throw an exception if the input is invalid. If you reason that will not happen in your situation, do not handle that case. The exception message generated by the framework is just fine.
I am trying to get the total amount of time in hours between when a job is logged in a ticketing system to when then job is due. For example if the job is logged at 4:00pm on a Friday and is due at 10:30am on Monday it will have a 3 hour time span. (excluding weekends and our of business hours)
I can get the TimeSpan for total time but I have no idea how to get the TimeSpan in business hours only?
I am using LINQpad as a code scratchpad and I am querying a database.
Below is an example of the my query which gets the total TimeSpan rather than the TimeSpan in Business Hours.
(from so in TblServiceOrders
where so.DateReceived != null
&& so.TimeReceived != null
&& so.DateRequested != null
&& so.TimeRequested != null
orderby so.SONumber descending
select new
{
so.SONumber,
received = so.DateReceived.Value.AddTicks(so.TimeReceived.Value.Ticks),
requested = so.DateRequested.Value.AddTicks(so.TimeRequested.Value.Ticks),
TimeSpan = (so.DateReceived.Value.AddTicks(so.TimeReceived.Value.Ticks))
.Subtract(so.DateRequested.Value.AddTicks(so.TimeRequested.Value.Ticks))
}).Take(20)
I hope there are better ways to do this however since my TimeRange is never longer than a few days I have been able to achieve it all within the Linq Query with the below code:
This only works with LINQ-to-SQL which is unfortunate as it cannot be used in Entity Framework.
from so in TblServiceOrders
where so.DateReceived != null &&
so.TimeReceived != null &&
so.DateRequested != null &&
so.TimeRequested != null
//Get the named day of the week for start of TimeRange
let DayReceived = so.DateReceived.Value.DayOfWeek
let DayRequested = so.DateRequested.Value.DayOfWeek
//Find out how many days pass within the TimeRange
let SLADaysLapesed = ((so.DateRequested.Value.Day) - (so.DateReceived.Value.Day))
//Find out if any of those days past fall onto a Sat and therefor over a weekend.
//This will only work for the first weekend within the TimeRange.
let SLAContainsWE = (DayReceived == DayOfWeek.Monday && SLADaysLapesed <= 4 ||
DayReceived == DayOfWeek.Tuesday && SLADaysLapesed <= 3 ||
DayReceived == DayOfWeek.Wednesday && SLADaysLapesed <= 2 ||
DayReceived == DayOfWeek.Thursday && SLADaysLapesed <= 1 ||
DayReceived == DayOfWeek.Friday && SLADaysLapesed <= 0 ? false : true )
//Work out how many hours pass outside of business hours. (930 minutes)
let SLATotalDailyAH = (new TimeSpan(0, (930 * SLADaysLapesed), 0))
//Work out if the TimeRange falls over a weekend. Time declared in mins is equal to two standard working days 8.5hr x 2 in my case.
let TotalWEHours = (SLAContainsWE == true ? new TimeSpan(0,1020,0) : new TimeSpan(0,0,0))
//Work out how many Business hours have passed by removing the total after hours.
let SLAHours = (
((so.DateRequested.Value.AddHours(so.TimeRequested.Value.Hour)).AddMinutes(so.TimeRequested.Value.Minute)).Subtract
((so.DateReceived.Value.AddHours(so.TimeReceived.Value.Hour)).AddMinutes(so.TimeReceived.Value.Minute)).Subtract
(SLATotalDailyAH).Subtract(TotalWEHours)
)
select SLAHours
I'm making a little alarm clock as a project to practice as I'm just a beginner.
I got 2 textboxes in which the user can put in the hours and minutes at which he wants the alarm to go off.
How do i check if the alarm time provided by the user is the same as the time on his system / pc?
Use
int hours = System.DateTime.Now.Hour;
int minutes = System.DateTime.Now.Minute;
if(Convert.Toint32(txtHours.Text) == hours && Convert.Toint32(txtMinutes.Text) == minutes)
{
// same time
}
else
{
// time not same
}
Here is a litle sample to get you on your way
int myMinute = 5;
int myHour = 19;
if (DateTime.Now.Minute == myMinute && DateTime.Now.Hour == myHour)
{
}
All above answers are helpful but you should make in practice to use TimeSpan for comparing date or time.
int hour=5;
int min=20;
int sec=0;
TimeSpan time = new TimeSpan(hour, min, sec);
TimeSpan now = DateTime.Now.TimeOfDay;
// see if start comes before end
if (time == now)
{
//put your condition
}
Please see this url for more info.
How to check if DateTime.Now is between two given DateTimes for time part only?
var current = DateTime.Now;
if (current.Hour == 9 && current.Minute == 0) {
//It is 9:00 now
}
if(Convert.ToInt32(tbHours.Text) == DateTime.Now.Hours
&& Convert.ToInt32(tbMinutes.Text) == DateTime.Now.Minutes)
{
//set off alarm
}
The problem:
I am in process of implementing a scheduler for my advisor in school. The scheduler supposes to setup a 15 minutes interval time slot from 8:00 AM to 5:00 PM, Monday to Friday. In addition, the advisor will have to specify the start and end dates of the scheduler. The scheduler will also feature an option to specify if the 15 minutes time slot is not open. Meaning my advisor will be able to mark specific time slot as NOT AVAILABLE.
What I have so far:
I have created a simple class:
public class TimeSlot
{
public DateTime dateTime
{
get;
set;
}
public bool isAvailable
{
get;
set;
}
TimeSlot(DateTime dt, bool Avalible)
{
dateTime = dt;
isAvailable = Avalible;
}
}
The class basically represents an object for one time slot in the scheduler. I also have a list of time slots that keeps a list of the valid time slots:
List<TimeSlot> TSList = new List<TimeSlot>();
Note that a valid time slot means the following:
Date is within: Monday to Friday.
Time is within: 8:00 AM to 5:00 PM
Time slots are within: 15 minutes interval.
In addition, I have a method that fill in the TSList as the following:
private void button_Next_Click(object sender, RoutedEventArgs e)
{
/* Getting the values of fromDate and toDate from the GUI controls*/
DateTime fromDate = datePicker1.SelectedDate.Value;
DateTime toDate = datePicker2.SelectedDate.Value;
while (fromDate <= toDate)
{
/*This ensures that we only deal with days Monday to Friday*/
if (fromDate.DayOfWeek.ToString() != "Saturday" && fromDate.DayOfWeek.ToString() != "Sunday")
{
/*PROBLEM HERE!!*/
}
/*Updating fromDate: Incrementing fromDate by 1 day*/
fromDate = fromDate.AddDays(1);
}
}
Notes that I was only able to satisfy the first condition in my valid time slot conditions. Thus, I was only able to restrict the dates to be within Monday to Friday range.
The questions:
I am trying to achieve the missing two valid conditions for a time slot:
How to restrict the times to be only 8:00am to 5:00 pm?
How to make time slots separated by 15 minutes interval?
First, please use DayOfWeek.Saturday and DayOfWeek.Sunday for the comparision, converting to a string is not necessary...
Then just use a simple loop like
DateTime startSlot = fromDate.Date.AddHours(8); // Starts at 8:00AM
while (startSlot.Hour < 17) {
// Construct time slot class
startSlot = startSlot.AddMinutes(15);
}
This gives you startSlot values starting at 8:00am at every date ranging to 5pm (i.e. the last one is 4:45pm).
Why are you considering building this out of nothing?
Why are you not starting with one of the many calendar management programs that are available off the shelf? For example, Microsoft Outlook contains calendar and schedule management, and you can do all of what you describe, easily. It also integrates with other scheduling tools via .ICS files, it syncs with mobile devices, syncs with Google Calendar, and so on.
But there are lots of other options. Google Calendar is another obvious one.
I don't know why you would ever consider starting from scratch. Unless it's an academic exercise (and no, I don't mean that you work in academia), then you should use larger building blocks to start.
It's like building a structure, starting with sand and water, instead of pre-fabricated concrete block.
Just quick implementation. Let me know if you need some comments.
// Round interval
const int roundInterval = 15;
var remainder = fromDate.TimeOfDay.Minutes % roundInterval;
var curTime = remainder == 0 ? fromDate : fromDate.AddMinutes(roundInterval - remainder);
curTime = curTime.AddSeconds(-curTime.TimeOfDay.Seconds);
var delta = TimeSpan.FromMinutes(roundInterval);
while (curTime < toDate)
{
while (curTime.DayOfWeek == DayOfWeek.Saturday || curTime.DayOfWeek == DayOfWeek.Sunday)
{
curTime = curTime.Date.AddDays(1);
}
if (curTime.TimeOfDay.Hours < 8)
{
curTime = curTime.AddHours(8 - curTime.TimeOfDay.Hours);
curTime = curTime.AddMinutes(-curTime.TimeOfDay.Minutes);
continue;
}
if (curTime.TimeOfDay.Hours >= 17)
{
curTime = curTime.AddHours(24 - curTime.TimeOfDay.Hours);
curTime = curTime.AddMinutes(-curTime.TimeOfDay.Minutes);
continue;
}
TSList.Add(new TimeSlot(curTime, true));
curTime = curTime.Add(delta);
}
}
DateTime myScheduledTimeSlot = new DateTime(2010, 10, 26, 8, 45, 0);
// Use existing check to check day of week constraint...
// Check if the datetime falls on a correct minute boundary
switch (myScheduledTimeSlot.Minute)
{
case 0:
case 15:
case 30:
case 45:
// The time slot is valid
break;
default:
// The time slot is not valid
break;
}
It is pretty simple to check whether it falls in a 15 minute slot as you don't have weird boundaries keeping every hour identical. I'd recommend checking out Quart.NET if you want to save some time doing eventing/scheduling.