Humanizer for DateTime - c#

I have this code:
Configurator.DateTimeHumanizeStrategy = new PrecisionDateTimeHumanizeStrategy(.75);
var dateTime1 = DateTime.UtcNow.AddYears(2).AddMonths(-5);
var text1 = dateTime1.Humanize();
In the text1 variable I get "one year from now". But this is not very accurate. Is there any way to get "one year and seven months from now"?
Update 1:
Solution #Daniel Hoffman has some problems, for example if my date is in the past:
//UtcNow is 11.07.2021
var dateTime6 = new DateTime(2021, 4, 24);
TimeSpan dateTimeSpan6 = dateTime6 - DateTime.UtcNow;
var text6 = dateTime6.Humanize();
string textSpan6 = dateTimeSpan6.Humanize(maxUnit: TimeUnit.Year, precision: 2);
then I get "2 months, 11 weeks" which contains basically the same information twice but in different units.
Update 2:
I have fixed the problem with dates in the past, by using Duration() method:
var timeSpan = date - DateTime.UtcNow;
return timeSpan.Duration().Humanize(maxUnit: TimeUnit.Year, precision: 2, minUnit: TimeUnit.Day);

[Edit]: Using TimeSpan will allow you to specify the precision of your period, but you will lose the ability to have "yesterday" or
"tomorrow", and it omits the " ago" or " from now", all of which are
localized.
A partial workaround would be to use the TimeSpan.Humanize
method for TimeSpans less than 366 days and DateTime.Humanize
otherwise. And if it's only going to be used in one language, the user
can append the appropriate text depending on if the timespan is
negative.
You can use the precision parameter with a TimeSpan:
TimeSpan periodFromNow = DateTime.UtcNow.AddYears(2).AddMonths(-5) - DateTime.UtcNow;
Then:
string myPeriodFromNow = periodFromNow.Humanize(maxUnit: TimeUnit.Year, precision: 2);
Other examples:
TimeSpan.FromDays(486).Humanize(maxUnit: TimeUnit.Year, precision: 7) => "1 year, 3 months, 29 days" // One day further is 1 year, 4 month
TimeSpan.FromDays(517).Humanize(maxUnit: TimeUnit.Year, precision: 7) => "1 year, 4 months, 30 days" // This month has 30 days and one day further is 1 year, 5 months
See also: https://github.com/Humanizr/Humanizer#humanize-timespan

It seems like its not currently possible in Humanizer to do what you want.
Check out this method PrecisionHumanize() on line 102, if the amount of days exceeds 365 then only years will be returned. And in general it seems like only one type of length of time can be returned, there is no years and months or minutes and seconds, just the largest one.
But check out another library called NodaTime it might be able to do what you want.
Here is a link to a different question similar to yours.

Related

C#: Convert 3 Variables into 24 Hours Format

I'm new to C# and i need help on converting 3 int variables into 24 hour format. I look on other Stack Overflow questions but it mostly only convert 1 variable into DateTime meanwhile i need to convert 3 variables into to 24 hour format. Here's what the variable looks like
private int hours = 1;
private int minutes = 1;
private int seconds = 1;
my expected outcome is 01:01:01 but i don't know how to do that.
You can do it like this:
var dt= new DateTime(1, 1, 1, hours, minutes, seconds); // year, month, day, hours, minutes, seconds
If you want to cast to string after that, you can do:
dt.ToString("HH:mm:ss"); // 01:01:01 // 24 hour clock digits
dt.ToString("hh:mm:ss tt"); // 01:01:01 AM // 12 hour clock
I already found a answer that works for me since i can't insert the variables like the previous 2 answers. So i just use
hours.ToString("00")
minutes.ToString("00")
seconds.ToString("00")
this makes it into a string in 2 digit number like my expected outcome.

C# TimeSpan calculating hours

I am trying to create a time card solution in C# and having an issue with totaling hours worked for a week. From a drop down, the user would select the number of hours they worked in a day (ex. 5:30 - the 5:30 is the total hours worked, not the actual time 5:30). The user would select the hours each work day and the application would then total the hours for the week. The application I have written totals the hours, but I have two issues: if I use .Hours to add the hours up, I run into an issue when the total goes over 24; when I use .TotalHours, it calculates over 24 ok, but somehow it adds an hour randomly when I select :30 increments. Here is the code I have to calculate and display the totals:
using .Hours does not allow the total number of hours to go over 24. Instead it converts the 24 to 1 day and starts the adding the hours again, losing the original 24:
lblWorkgroupOneTotalTime.Text = (totalWeekOneHours.Hours).ToString("00") +
":" + (totalWeekOneHours.Minutes).ToString("00");
//using .TotalHours causes the calculation to randomly add an hour to the total:
lblWorkgroupTwoTotalTime.Text =
(totalWeekTwoHours.TotalHours).ToString("00").TrimStart('0') +
":" + (totalWeekTwoHours.Minutes).ToString("00");
I feel like I am very close to having everything work correctly, but I can't figure this part out.
How about this:
Initialize an example for 30 hours and 30 minutes:
TimeSpan totalWeekThreeHours = new TimeSpan(30, 30, 0);
(Timespan works better than DateTime here I feel.)
Then:
var hours = (int)totalWeekThreeHours.TotalMinutes / 60;
var mins = totalWeekThreeHours.TotalMinutes % 60;
Output:
var example1 = hours + ":" + mins;
var example2 = String.Format("{0} hours {1} mins", hours, mins);
Console.WriteLine("Example 1: " + example1);
Console.WriteLine("Example 2: " + example2);
//Output:
//Example 1: 30:30
//Example2: 30 hours 30 minutes
it adds an hour randomly
Nothing in programming happens "randomly". So when debugging, your first step should always be to look for patterns in your bug. As long as you believe the bug happens "randomly", you will have a mental block getting in the way of finding the bug.
As for your specific issue…
For any of the Total... properties of TimeSpan, this will be a double value that represents the entire time span in the units you're retrieving, including any fractional amounts.
For example, if the TimeSpan value represents 1 hour and 45 minutes, the TotalHours value will be 1.75. At the same time, you are telling the ToString() method that you want the value rounded to the nearest integer value. So, any time that the fractional part of your time span in hours is greater than one-half, the value is rounded up to the next hour value.
If you don't want that behavior, you should just truncate the value yourself before formatting it as a string:
lblWorkgroupTwoTotalTime.Text = string.Format("{0:0}:{1:00}",
(int)totalWeekTwoHours.TotalHours, totalWeekTwoHours.Minutes);
I also don't see why you used the format string "00" only to strip off the leading 0 after the fact. Easier to just not format the string that way in the first place.
Finally, note alternative syntax for formatting strings. Your approach (calling ToString() explicitly) is fine, but I find it wordy. The above is more concise, and does a better job separating the format from the input values.
The problem with displaying TotalHours with a format string of "00" is that it's going to round up. You have a couple of choices if you don't want to show days:
Use Hours + Days * 24 for the hours
Use TotalMinutes / 60 for hours
Convert TotalHours to an int, which will always round down
For example:
var totalHours = (totalWeekOneHours.Days * 24) + totalWeekOneHours.Hours;
// Or:
var totalHours = totalWeekOneHours.TotalMinutes / 60;
// Or:
var totalHours = (int)totalWeekOneHours.TotalHours;
Then you can output it:
lblWorkgroupOneTotalTime.Text = $"{totalHours:00}:{totalWeekOneHours.Minutes:00}";

Calculate approximate date from approximate timespan

Since Sony changed how their trophy server authentication works, I am looking for an alternative solution to approximate trophy dates. The UK Playstation Network website has a string underneath unlocked trophies approximating how long ago trophies were earned. I have the following code:
private TimeSpan ParseTimeSpan(string datestring)
{
TimeSpan ts = new TimeSpan();
datestring = datestring.Replace("Attained:", ""); // strip Attained:
string[] parts = datestring.Split(' ');
int numeric = int.Parse(parts[0]);
string duration = parts[1];
switch(duration)
{
case "minutes":
ts = new TimeSpan(0, numeric, 0);
break;
case "hours":
ts = new TimeSpan(numeric, 0, 0);
break;
case "days":
ts = new TimeSpan(numeric, 0, 0, 0);
break;
case "months":
ts = new TimeSpan(numeric * 30, 0, 0);
break;
case "years":
ts = new TimeSpan(numeric * 365, 0, 0);
break;
}
return ts;
}
If I have the string Attained:17 months ago underneath one of my trophies, it should take the current month and subtract 17 months. If I have the string Attained:3 hours ago, it should take the current date and subtract 3 hours to approximate an earned date. If I have the string Attained: 5 minutes ago, it should take the current date and subtract 5 minutes to approximate an earned date.
My plan is have this code run as a web service and accompany with a desktop client. I'm unsure of whether I should return a TimeSpan or do just calculate the date directly? Is there a better approach? Not all months are 30 days and some years are more than 365 days (leap years) so doing hard coded calculations won't necessarily work.
You don't have to worry about some months having 30 days and some not. If you have the timespan you can directly decrement the current date by that span.
TimeSpan ts = new TimeSpan(5,0,0);
var earned = DateTime.Now - ts;
Console.Write(earned);
This can be done in or out of your service, but I would perform it in the service if the actual date is what's needed.
Also you can add a regular expression if you need to add some flexibility later on.
string input = "Attained:17 months ago";
string pattern = #"Attained:(?<value>[0-9]+) (?<unit>(years|months|days)) ago";
var match = Regex.Match(input, pattern);
if(match.Success)
{
int value = Int32.Parse(match.Groups["value"].Value);
string unit = match.Groups["unit"].Value;
Console.WriteLine(value);
Console.WriteLine(unit);
}
Could make unit an enum as well
Noda Time is a much more flexible framework for working with time and date values than the types provided in the .NET base class library. It includes, among other things, a Period type, which represents some amount of time in elapsed years, months, days, etc. The actual elapsed time represented is unfixed. As you say, some months or years are longer than others, so Period is the type to use if you are not dealing with a fixed amount of time (as opposed to a fixed Duration). A Period can be added to the various date/time types in the Noda Time library.
While you can add and subtract a TimeSpan from a DateTime[Offset], it's not a terribly useful type if you need to deal with units of time longer than hours. Consider using Noda Time for nontrivial time/date computations.

Behavior of DateTime.AddYears on leap year

Can anyone explain the mathematical or simply the reasoning behind the leap year calculations in .NET when using AddYears method on DateTime?
If you take the 29th Feb 2012 and add a year, you get the 28th Feb 2013, not the 1st Mar 2013 (day before one year later).
If you add one year to 31st Jan 2012, you get 31st Jan 2013 (same date one year later).
I think most people would assume that "one year from 29.02.leapX is 01.03.leapX+1".
Example:
// Testing with 29th Feb
var now1 = DateTime.Parse("2012-02-29 15:00:00");
var results1 = new DateTime[]
{
now1.AddYears(1),
now1.AddYears(2),
now1.AddYears(3),
now1.AddYears(4)
};
foreach(var dt in results1)
{
Console.WriteLine(dt.ToString("s"));
}
// Output:
// 2013-02-28T15:00:00
// 2014-02-28T15:00:00
// 2015-02-28T15:00:00
// 2016-02-29T15:00:00
// Testing with 31st Jan
var now2 = DateTime.Parse("2012-01-31 13:00:00");
var results2 = new DateTime[]
{
now2.AddYears(1),
now2.AddYears(2),
now2.AddYears(3),
now2.AddYears(4)
};
foreach(var dt in results2)
{
Console.WriteLine(dt.ToString("s"));
}
// Output:
// 2013-01-31T13:00:00
// 2014-01-31T13:00:00
// 2015-01-31T13:00:00
// 2016-01-31T13:00:00
I think most people would assume that "one year from 29.02.leapX is 01.03.leapX+1".
I wouldn't. I would normally expect truncation. It's fundamentally similar to adding one month to January 30th - I'd expect to get the last day in February. In both cases, you're adding a "larger unit" (month or year) and a "smaller unit" (day) is being truncated to fit in with the year/month combination.
(This is how Joda Time and Noda Time behave too, btw.)
As Tim mentioned in comments, it's documented that way too:
The AddYears method calculates the resulting year taking into account leap years. The month and time-of-day part of the resulting DateTime object remains the same as this instance.
So the month has to stay as February; the year will change based on how many years are being added, obviously - so the day has to adjust to stay valid.
With your rationale then 1-Mar-2012 would become 2-Mar-2012 when you added a year. If you add this shift for all prior leap years then you are going to find your calculation massively adrift. The only sensible response is to return 28-Feb for non-leap years.
It is interesting, nether-the-less ..
e.g. this function is sometimes used:
private static int Age(DateTime birthDate, DateTime asAtDate)
{
// Calculate age in years
int age = asAtDate.Year - birthDate.Year;
if (asAtDate < birthDate.AddYears(age)) age--;
if (age < 0) age = 0;
return age;
}
If a person was born on 29Feb2016, this function is going to conclude they have reached age 1 on 28Feb2017.
I noted Excel Function examples as per:
=DATEDIF(DATE(2016,2,28),DATE(2017,2,28),"Y")
gives result of 1
=DATEDIF(DATE(2016,2,29),DATE(2017,2,28),"Y")
gives result of 0
=DATEDIF(DATE(2016,2,29),DATE(2017,3,1),"Y")
gives result of 1
=DATEDIF(DATE(2016,3,1),DATE(2017,3,1),"Y")
gives result of 1

Exact c# result of sql datediff

I'm trying to get the number of days (calculated byu datediff) in sql and the number of days in c# (calculated by DateTime.now.Substract) to be the same, but they return different results....
//returns 0
int reso = DateTime.Now.Subtract(expirationDate).Days;
vs
//returns 1
dateDiff(dd,getDate(),ExpirationDate)
In both cases, ExpirationDate is '10/1/2011 00:00:00', and the code and the DB are sitting on the same server. I want the return int to be the same. I suspect I'm missing something stupid... ideas??
dateDiff(dd,getDate(),ExpirationDate) Is doing a days comparison. DateTime.Now.Subtract(expirationDate).Days is doing a date and time
For example
SELECT dateDiff(dd,'10/1/2011 23:59:00' , '10/2/2011') returns one day even when only one minute apart.
If you want the same in C# you need to remove the time component
e.g.
DateTime dt1 = new DateTime(2011,10,1, 23,59,0);
DateTime dt2 = new DateTime(2011,10,2, 0,0,0);
Console.WriteLine((int) dt2.Subtract(dt1.Subtract(dt1.TimeOfDay)));
So in your case it would be something like
DateTime CurrentDate = DateTime.Now;
int reso = CurrentDate.Subtract(CurrentDate.TimeOfDay).Subtract(DateTime.expirationDate).Days;
I haven't tested it but I would not do
DateTime.Now.Subtract(DateTime.Now.Subtract.TimeOfDay)
Because the second call to Now wouldn't be guaranteeing to be the same as first call to Now
In any case Stealth Rabbi's answer seems more elegant anyway since you're looking for a TimeSpan not a DateTime
10/1/2011 is less than 1 day away from DateTime.Now. Since you're getting back a TimeSpan and then applying Days to it, you're getting back a TimeSpan that is < 1 day. So it'll return 0 Days.
Instead, just use the Date component of those DateTimes and it'll correctly report the number of days apart - like this:
DateTime now = DateTime.Now;
DateTime tomorrow = new DateTime(2011, 10, 1);
var val = (tomorrow.Date - now.Date).Days;
This will yield you 1 day.
I'm assuming you want the number of Total days, not the number of days from the largest previous unit. You'd want to use the TotalDays property. Also, you may find it easier to use the minus operator to do a subtraction
DateTime d1 = DateTime.Now;
DateTime d2 = new DateTime(2009, 1, 2);
TimeSpan difference = d1 - d2;
Console.WriteLine(difference.TotalDays); // Outputs (today):1001.46817997424

Categories