Related
Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 3 years ago.
Improve this question
If I have the age of a person, and the current day and month are the same as the birthdate, how can I calculate the birthyear with Java 8?
For example:
Age = 30 years
Current date = July 1st, 2019
Expected output = July 1st, 1989
tl;dr
In other words, you want to subtract a number of years from a date.
LocalDate
.of( 2019 , Month.JULY , 1 )
.minusYears( 30 )
See this code run live at IdeOne.com.
1989-07-01
LocalDate
The LocalDate class represents a date-only value without time-of-day and without time zone or offset-from-UTC.
A time zone is crucial in determining a date. For any given moment, the date varies around the globe by zone. For example, a few minutes after midnight in Paris France is a new day while still “yesterday” in Montréal Québec.
If no time zone is specified, the JVM implicitly applies its current default time zone. That default may change at any moment during runtime(!), so your results may vary. Better to specify your desired/expected time zone explicitly as an argument. If critical, confirm the zone with your user.
Specify a proper time zone name in the format of Continent/Region, such as America/Montreal, Africa/Casablanca, or Pacific/Auckland. Never use the 2-4 letter abbreviation such as EST or IST as they are not true time zones, not standardized, and not even unique(!).
ZoneId z = ZoneId.of( "America/Montreal" ) ;
LocalDate today = LocalDate.now( z ) ;
If you want to use the JVM’s current default time zone, ask for it and pass as an argument. If omitted, the code becomes ambiguous to read in that we do not know for certain if you intended to use the default or if you, like so many programmers, were unaware of the issue.
ZoneId z = ZoneId.systemDefault() ; // Get JVM’s current default time zone.
Or specify a date. You may set the month by a number, with sane numbering 1-12 for January-December.
LocalDate ld = LocalDate.of( 1986 , 2 , 23 ) ; // Years use sane direct numbering (1986 means year 1986). Months use sane numbering, 1-12 for January-December.
Or, better, use the Month enum objects pre-defined, one for each month of the year. Tip: Use these Month objects throughout your codebase rather than a mere integer number to make your code more self-documenting, ensure valid values, and provide type-safety. Ditto for Year & YearMonth.
LocalDate ld = LocalDate.of( 1986 , Month.FEBRUARY , 23 ) ;
MonthDay
I have a hunch what you really need is MonthDay to represent the idea of an annual birthday, just the month and the day-of-month but without a year.
From a MonthDay object you can determine a date for any year, generating a LocalDate object. Use Year class to get current year.
MonthDay birthMonthDay = MonthDay.of( Month.JANUARY , 23 ) ;
LocalDate birthDateSomeYear = birthMonthDay.atYear( 2010 ) ;
LocalDate birthDateThisYear = birthMonthDay.atYear( Year.now( z ).getValue() ) ;
A month-day of February 29th will be adjusted to February 28th in a non-leap year.
Date math
You can subtract a number of years from a LocalDate, producing a new LocalDate object.
int ageInYears = 37 ;
LocalDate then = someLocalDate.minusYears( ageInYears ) ;
If you meant you have the age in a finer resolution of years-months-days, use Period class.
Period p = Period.of( 37 , 4 , 2 ) ; // years-months-days.
Do the math.
LocalDate ld = someLocalDate.minus( p ) ;
I am beginner for Java, so asked. but I got solution. Thank you guys for suggestion.
public class test {
public static void main(String[] args) {
// TODO Auto-generated method stub
int age = 30;
Date today = new Date();
Calendar cal = Calendar.getInstance();
cal.setTime(today);
int dayOfYear = cal.get(Calendar.YEAR);
int birthYear = dayOfYear - age;
System.out.println(birthYear);
// using local time
LocalDate todayDate = LocalDate.now();
int y = todayDate.getYear();
int birthYearFromTime = y-age;
System.out.println(birthYearFromTime);
}
}
I have to fetch hours and minutes from time format I am getting from the system however it is showing me error like int month= moment.Month;
DateTime productDate = DateTime.Now;
string twentyFourHourFormatHour =int.Parse(productDate.ToString("HH")).ToString();
Not picking up productdate when trying to store it in string (line 2)
DateTime has properties to expose all of the data you need. There is no need to do elaborate string manipulation.
DateTime date = DateTime.Now();
int hour = date.Hour;
int minute = date.Minute;
The topic at hand is a messy domain-specific problem working with dates in Oracle's ERP software called JD Edwards. Its detail is documented in this question.
Before writing wrapper classes for handling the dates and times from JD Edwards, I want to know if JodaTime or Java 8 introduced any special support for this unique time format, or if I'll have to do significant string manipulation regardless of the libraries I use.
This is an obscure problem, so please only respond if you have specific knowledge of this problem, and/or JodaTime/Java 8/JSR 310.
ADDITION:
Per Basil Bourque's request, adding example of timestamps that accompany said dates. Here are two example of date/time fields from different tables:
JCSBMDATE:115100, JCSBMTIME:120102.0
RLUPMJ:114317, RLUPMT:141805.0
Also, the date variable is being cast as a BigDecimal and the time is a Double. So, I'll probably keep the string parsers around, but also write factory methods that take the BigDecimal/Double values natively as well.
It seems that the time field is actually the number of Milliseconds (not seconds) from the start of the day, and the ".0" can be ignored. So, one will have to perform a conversion and calculation like so:
localDate.atTime(LocalTime.ofNanoOfDay(Long.parseLong(jdeTime) * 1000000))
JD Edwards date defined
Actually the detail of a JD Edwards date is not so gory, according to this simple description on a page at Oracle.com:
About the Julian Date Format
Date fields in JD Edwards World files are stored in the Julian format. …
The Julian (*JUL) date format is CYYDDD, where:
C is added to 19 to create the century, i.e. 0 + 19 = 19, 1 + 19 = 20. YY is the year within the century, DDD is the day in the year.
Terms:
I would call the C part a “century-offset”, how many centuries to add to 19. Use 0 for 19xx years, and 1 for 20xx years.
The java.time framework calls the DDD a “DayOfYear”, and “ordinal date” is another term. The use of “Julian” for a day-number-within-a-year is common but not correct, conflicting with a Julian Day.
The java.time framework does not include direct support for parsing or generating strings of this format, not that I can find.
JulianFields
There is the java.time.temporal.JulianFields but those are for an redefined version of Julian dates where we count the number of days from an epoch (1970-01-01 (ISO) rather than the historic November 24, 4714 BC (proleptic Gregorian)), while ignoring years altogether. So this has nothing to do with the JD Edwards definition, contrary to some incorrect advice on that page linked in the Question.
Ordinal Date
This JD Edwards date is a version of an ordinal date. The ordinal date is sometimes referred to casually (and incorrectly) as a "julian" date only because it shares the idea of counting a sequence of days. But an ordinal date counts days from the beginning of the year to end of year for a number always between 1 and 365/366 (leap year), not counting since some epoch and growing into a number into the thousands.
Back to the Question, handling the JD Edwards date in java.time…
No, I do not find any direct or indirect support the JD Edwards date built into java.time.
The java.date.format package seems unaware of the century of a date, only the year and the era. So no way that I can find to define the C part of a JD Edwards date.
The last part of a JD Edwards date, the ordinal number of days in the year, is well-handled with within both the date-time classes and the formatting classes.
Wrap LocalDate
Since a JD Edwards date apparently has the same logic as the ISO chronology used by java.time, the only real issue at hand is parsing and generating String objects according to this particular format. All other behavior can be leveraged from a LocalDate.
Since I cannot find a way to define a java.time.format.DateTimeFormatter for this purpose, I suggest writing a utility class to handle these chores.
Ideally we would extend the LocalDate class, overriding its parse and toString methods. And perhaps a getCenturyOffset method. But the LocalDate class is marked final and cannot be extended. So I would create something like this class shown below, wrapping a LocalDate.
CAVEAT: Use at your own risk. Fresh code, barely run, hardly tested. Meant as an example, not for use in production. Use according to terms of the ISC License.
package com.example.whatever;
import java.time.LocalDate;
import java.time.ZoneId;
/**
* Wraps a 'LocalDate' to provide parsing/generating of strings in format known
* as JD Edwards date.
*
* Format is CYYDDD where C is the number of centuries from 1900, YY is the year
* within that century, and DDD is the ordinal day within the year (1-365 or
* 1-366 in Leap Year).
*
* Immutable object. Thread-safe (hopefully! No guarantees).
*
* I would rather have done this by extending the 'java.time.LocalDate' class, but that class is marked 'final'.
*
* Examples: '000001' is January 1 of 1900. '116032' is February 1, 2016.
*
* © 2016 Basil Bourque. This source code may be used according to terms of the ISC License at https://opensource.org/licenses/ISC
*
* #author Basil Bourque
*/
public class JDEdwardsLocalDate {
private LocalDate localDate = null;
private int centuryOffset;
private int yearOfCentury;
private String formatted = null;
// Static Factory method, in lieu of public constructor.
static public JDEdwardsLocalDate from ( LocalDate localDateArg ) {
return new JDEdwardsLocalDate ( localDateArg );
}
// Static Factory method, in lieu of public constructor.
static public JDEdwardsLocalDate parse ( CharSequence charSequenceArg ) {
if ( null == charSequenceArg ) {
throw new IllegalArgumentException ( "Passed CharSequence that is null. Message # 0072f897-b05f-4a0e-88d9-57cfd63a712c." );
}
if ( charSequenceArg.length () != 6 ) {
throw new IllegalArgumentException ( "Passed CharSequence that is not six characters in length. Message # eee1e134-8ec9-4c92-aff3-9296eac1a84a." );
}
String string = charSequenceArg.toString ();
// Should have all digits. Test by converting to an int.
try {
int testAsInteger = Integer.parseInt ( string );
} catch ( NumberFormatException e ) {
throw new IllegalArgumentException ( "Passed CharSequence contains non-digits. Fails to convert to an integer value. Message # 0461f0ee-b6d6-451c-8304-6ceface05332." );
}
// Validity test passed.
// Parse.
int centuryOffset = Integer.parseInt ( string.substring ( 0 , 1 ) ); // Plus/Minus from '19' (as in '1900').
int yearOfCentury = Integer.parseInt ( string.substring ( 1 , 3 ) );
int ordinalDayOfYear = Integer.parseInt ( string.substring ( 3 ) );
int centuryStart = ( ( centuryOffset + 19 ) * 100 ); // 0 -> 1900. 1 -> 2000. 2 -> 2100.
int year = ( centuryStart + yearOfCentury );
LocalDate localDate = LocalDate.ofYearDay ( year , ordinalDayOfYear );
return new JDEdwardsLocalDate ( localDate );
}
// Constructor.
private JDEdwardsLocalDate ( LocalDate localDateArg ) {
this.localDate = localDateArg;
// Calculate century offset, how many centuries plus/minus from 1900.
int year = this.localDate.getYear ();
int century = ( year / 100 );
this.yearOfCentury = ( year - ( century * 100 ) ); // example: if 2016, return 16.
this.centuryOffset = ( century - 19 );
// Format as string.
String paddedYearOfCentury = String.format ( "%02d" , this.yearOfCentury );
String paddedDayOfYear = String.format ( "%03d" , this.localDate.getDayOfYear () );
this.formatted = ( this.centuryOffset + paddedYearOfCentury + paddedDayOfYear );
}
#Override
public String toString () {
return this.formatted;
}
public LocalDate toLocalDate () {
// Returns a java.time.LocalDate which shares the same ISO chronology as a JD Edwards Date.
return this.localDate;
}
public int getDayOfYear () {
// Returns ordinal day number within the year, 1-365 inclusive or 1-366 for Leap Year.
return this.localDate.getDayOfYear();
}
public int getYear () {
// Returns a year number such as 2016.
return this.localDate.getYear();
}
public int getYearOfCentury () {
// Returns a number within 0 and 99 inclusive.
return this.yearOfCentury;
}
public int getCenturyOffset () {
// Returns 0 for 19xx dates, 1 for 20xx dates, 2 for 21xx dates, and so on.
return this.centuryOffset;
}
public static void main ( String[] args ) {
// '000001' is January 1, 1900.
JDEdwardsLocalDate jde1 = JDEdwardsLocalDate.parse ( "000001" );
System.out.println ( "'000001' = JDEdwardsLocalDate: " + jde1 + " = LocalDate: " + jde1.toLocalDate () + " Should be: January 1, 1900. " );
// '116032' is February 1, 2016.
JDEdwardsLocalDate jde2 = JDEdwardsLocalDate.parse ( "116032" );
System.out.println ( "'116032' = JDEdwardsLocalDate: " + jde2 + " = LocalDate: " + jde2.toLocalDate () + " Should be: February 1, 2016." );
// Today
LocalDate today = LocalDate.now ( ZoneId.systemDefault () );
JDEdwardsLocalDate jdeToday = JDEdwardsLocalDate.from ( today );
System.out.println ( "LocalDate.now(): " + today + " = JDEdwardsLocalDate: " + jdeToday + " to LocalDate: " + jdeToday.toLocalDate () );
}
}
When run.
'000001' = JDEdwardsLocalDate: 000001 = LocalDate: 1900-01-01 Should be: January 1, 1900.
'116032' = JDEdwardsLocalDate: 116032 = LocalDate: 2016-02-01 Should be: February 1, 2016.
LocalDate.now(): 2016-05-09 = JDEdwardsLocalDate: 116130 to LocalDate: 2016-05-09
JD Edwards time-of-day
As for JD Edwards time-of-day formats, I searched and could not find any documentation. If you know of some, please edit your Question to add links. The only mentions of JDE times seemed to be a count of seconds from midnight.
If that is the case (a count since midnight), the java.time.LocalTime class has you covered. A LocalTime can be instantiated and read as either:
Whole seconds since start of day ( withSecond, ofSecondOfDay )
Fractional seconds since start of day, with a resolution of nanoseconds ( withNano, ofNanoOfDay )
Nanosecond resolution means up to nine digits of a decimal fraction. No problem handling the six digits you mentioned. Just do the math, multiply/divide by 1_000L. Just be aware that means possible data loss as you could be truncating those last three digits of fraction (7th, 8th, 9th digits of decimal fraction) if the LocalTime value came from outside of JD Edwards data. [FYI, the old java.util.Date/.Calendar classes, as well as Joda-Time, are limited to milliseconds resolution, for three digits of decimal fraction.]
Not recommended: You could do some kind of combo class, composed of a LocalDate and a LocalTime. Or use a LocalDateTime. The key issue is time zone. If a JD Edwards date-time is always in a certain time zone such as UTC, then it might make sense to combine and use an OffsetDateTime . But if it has no specific time zone context, if the values are just a fuzzy idea of a date-time rather than specific points on the timeline, then use LocalDateTime as it has no time zone. If a JDE is always in UTC, use OffsetDateTime set to ZoneOffset.UTC. If you want to specify a time zone (an offset plus rules for handling anomalies such as DST), use ZonedDateTime.
Recommended: Use a LocalTime separately. I do not think you want to be using my JDEdwardsLocalDate class in your business logic, especially because it is not a full implementation fitting into the java.time framework. My intention is to use that class to immediately convert to LocalDate when you encounter a JDE date. Same goes for a JDE time-of-day, convert to LocalTime immediately. If their context is always UTC, create an OffsetDateTime with UTC, and then pass that around your business logic. Only go back to a JDE date & time when necessary (persisting to database column of that JDE type, or reporting to user expecting that JDE presentation).
OffsetDateTime odt = OffsetDateTime.of( myLocalDate , myLocalTime , ZoneOffset.UTC );
If the JDE date & time has some other context implied, then assign the intended time zone.
ZoneId zoneId = ZoneId.of( "America/Montreal" );
ZonedDateTime zdt = ZonedDateTime.of( myLocalDate , myLocalTime , zoneId );
Time zone is crucial here. You must understand the concepts in general. Be clear that LocalDate and LocalTime and LocalDateTime are not a moment on the timeline. They have no specific meaning until you adjust them into a time zone (or at least an offset-from-UTC).
My diagram of date-time types included on this Answer may help you if not familiar with the java.time types.
And you must understand the meaning of JDE date & time and their use in your apps/databases. As I could not find anything about JDE time, I could not learn anything about the JD Edwards intentions towards time zones. So I cannot suggest anything more specific.
No: Neither Joda Time nor Java 8 have support for JD Edwards time representations.
Here is a response to a question about calculating age in Java.
/**
* This Method is unit tested properly for very different cases ,
* taking care of Leap Year days difference in a year,
* and date cases month and Year boundary cases (12/31/1980, 01/01/1980 etc)
**/
public static int getAge(Date dateOfBirth) {
Calendar today = Calendar.getInstance();
Calendar birthDate = Calendar.getInstance();
int age = 0;
birthDate.setTime(dateOfBirth);
if (birthDate.after(today)) {
throw new IllegalArgumentException("Can't be born in the future");
}
age = today.get(Calendar.YEAR) - birthDate.get(Calendar.YEAR);
// If birth date is greater than todays date (after 2 days adjustment of leap year) then decrement age one year
if ( (birthDate.get(Calendar.DAY_OF_YEAR) - today.get(Calendar.DAY_OF_YEAR) > 3) ||
(birthDate.get(Calendar.MONTH) > today.get(Calendar.MONTH ))){
age--;
// If birth date and todays date are of same month and birth day of month is greater than todays day of month then decrement age
}else if ((birthDate.get(Calendar.MONTH) == today.get(Calendar.MONTH )) &&
(birthDate.get(Calendar.DAY_OF_MONTH) > today.get(Calendar.DAY_OF_MONTH ))){
age--;
}
return age;
}
This code works just fine, but why does it have this comparison:
(birthDate.get(Calendar.DAY_OF_YEAR) - today.get(Calendar.DAY_OF_YEAR) > 3)
I've gone so far as to create a giant spreadsheet of all the day differences in a year to try to see what cases it might be covering, but I don't see anything that the other comparisons don't cover. Can anyone explain the purpose behind including this comparison? Is it more efficient in some way?
Following code example from ThreetenBP (backport of Java-8) supports the statement that a day-of-year-check is unnecessary:
#Override
public long until(Temporal endExclusive, TemporalUnit unit) {
LocalDate end = LocalDate.from(endExclusive);
if (unit instanceof ChronoUnit) {
switch ((ChronoUnit) unit) {
case DAYS: return daysUntil(end);
case WEEKS: return daysUntil(end) / 7;
case MONTHS: return monthsUntil(end);
case YEARS: return monthsUntil(end) / 12;
case DECADES: return monthsUntil(end) / 120;
case CENTURIES: return monthsUntil(end) / 1200;
case MILLENNIA: return monthsUntil(end) / 12000;
case ERAS: return end.getLong(ERA) - getLong(ERA);
}
throw new UnsupportedTemporalTypeException("Unsupported unit: " + unit);
}
return unit.between(this, end);
}
[...]
private long monthsUntil(LocalDate end) {
long packed1 = getProlepticMonth() * 32L + getDayOfMonth(); // no overflow
long packed2 = end.getProlepticMonth() * 32L + end.getDayOfMonth(); // no overflow
return (packed2 - packed1) / 32;
}
The line case YEARS: return monthsUntil(end) / 12; (the expressions birthday.until(today, YEARS) and YEARS.between(birthday, today) are equivalent - one delegating to other) exploits the same algorithm as following reduced code cited by the OP and does not refer to any day-of-year-check:
age = today.get(Calendar.YEAR) - birthDate.get(Calendar.YEAR);
if (birthDate.get(Calendar.MONTH) > today.get(Calendar.MONTH)) {
age--;
}else if ((birthDate.get(Calendar.MONTH) == today.get(Calendar.MONTH )) &&
(birthDate.get(Calendar.DAY_OF_MONTH) > today.get(Calendar.DAY_OF_MONTH ))){
age--;
}
The question arises: Why the day-of-year-check?
a) the poster had originally taken the day-of-year-idea seriously and then forgotten to clean up in a later version
b) the poster hopes to "improve" the performance
Following Java-8-code demonstrates the problem of day-of-year-based algorithm if taken seriously and as complete version (the choice of library is not relevant here, only the algorithm matters):
LocalDate birthday = LocalDate.of(2001, 3, 6);
LocalDate today = LocalDate.of(2016, 3, 5); // leap year
int age = today.getYear() - birthday.getYear();
if (birthday.getDayOfYear() > today.getDayOfYear()) {
age--;
}
System.out.println("age based on day-of-year: " + age); // 15 (wrong)
System.out.println("age based on month and day-of-month: "
+ ChronoUnit.YEARS.between(birthday, today)); // 14 (correct)
Conclusion:
The proposed day-of-year-clause you had cited is only noise since the rest of the algorithm corresponds to what Java-8 does. Maybe the day-of-year-check originates from some earlier day-of-year-based versions of proposed code and had not been cleaned up yet.
In order to answer your last question: An unnecessary check like this is not good resp. efficient in terms of performance (although we talk here about micro-optimization).
Does anyone know of a good library (or code snippet) for converting a TimeSpan object to a "friendly" string such as:
Two years, three months and four days
One week and two days
(It's for a document expiry system, where the expiry could be anything from a few days to several decades)
Just to clarify, say I had a TimeSpan with 7 days, that should print "1 week", 14 days "2 weeks", 366 days "1 year and 1 day", etc etc.
I just stumbled upon this question because I wanted to do a similar thing. After some googling I still didn't find what I wanted: display a timespan in a sort of "rounded" fashion. I mean: when some event took several days, it doesn't always make sense to display the milliseconds. However, when it took minutes, it probably does. And in that case, I don't want 0 days and 0 hours to be displayed. So, I want to parametrize the number of relevant timespan parts to be displayed. This resulted in this bit of code:
public static class TimeSpanExtensions
{
private enum TimeSpanElement
{
Millisecond,
Second,
Minute,
Hour,
Day
}
public static string ToFriendlyDisplay(this TimeSpan timeSpan, int maxNrOfElements)
{
maxNrOfElements = Math.Max(Math.Min(maxNrOfElements, 5), 1);
var parts = new[]
{
Tuple.Create(TimeSpanElement.Day, timeSpan.Days),
Tuple.Create(TimeSpanElement.Hour, timeSpan.Hours),
Tuple.Create(TimeSpanElement.Minute, timeSpan.Minutes),
Tuple.Create(TimeSpanElement.Second, timeSpan.Seconds),
Tuple.Create(TimeSpanElement.Millisecond, timeSpan.Milliseconds)
}
.SkipWhile(i => i.Item2 <= 0)
.Take(maxNrOfElements);
return string.Join(", ", parts.Select(p => string.Format("{0} {1}{2}", p.Item2, p.Item1, p.Item2 > 1 ? "s" : string.Empty)));
}
}
Example (LinqPad):
new TimeSpan(1,2,3,4,5).ToFriendlyDisplay(3).Dump();
new TimeSpan(0,5,3,4,5).ToFriendlyDisplay(3).Dump();
Displays:
1 Day, 2 Hours, 3 Minutes
5 Hours, 3 Minutes, 4 Seconds
Suits me, see if it suits you.
Not a fully featured implementation, but it should get you close enough.
DateTime dtNow = DateTime.Now;
DateTime dtYesterday = DateTime.Now.AddDays(-435.0);
TimeSpan ts = dtNow.Subtract(dtYesterday);
int years = ts.Days / 365; //no leap year accounting
int months = (ts.Days % 365) / 30; //naive guess at month size
int weeks = ((ts.Days % 365) % 30) / 7;
int days = (((ts.Days % 365) % 30) % 7);
StringBuilder sb = new StringBuilder();
if(years > 0)
{
sb.Append(years.ToString() + " years, ");
}
if(months > 0)
{
sb.Append(months.ToString() + " months, ");
}
if(weeks > 0)
{
sb.Append(weeks.ToString() + " weeks, ");
}
if(days > 0)
{
sb.Append(days.ToString() + " days.");
}
string FormattedTimeSpan = sb.ToString();
In the end, do you really need to let someone know a document is going to expire exactly 1 year, 5 months, 2 weeks, and 3 days from now? Can't you get by with telling them the document will expire over 1 year from now, or over 5 months from now? Just take the largest unit and say over n of that unit.
There is now also the Humanizer project that looks very interesting that can do this and way more.
Here is my solution to this. It is based on other answers in this thread, with added support for year and month as that was requested in the original question (and was what I needed).
As for the discussion whether or not this makes sense I would say that there are cases where it does so. In my case we wanted to show the duration of agreements that in some cases are just a few days, and in other cases several years.
Tests;
[Test]
public void ToFriendlyDuration_produces_expected_result()
{
new DateTime(2019, 5, 28).ToFriendlyDuration(null).Should().Be("Until further notice");
new DateTime(2019, 5, 28).ToFriendlyDuration(new DateTime(2020, 5, 28)).Should().Be("1 year");
new DateTime(2019, 5, 28).ToFriendlyDuration(new DateTime(2021, 5, 28)).Should().Be("2 years");
new DateTime(2019, 5, 28).ToFriendlyDuration(new DateTime(2021, 8, 28)).Should().Be("2 years, 3 months");
new DateTime(2019, 5, 28).ToFriendlyDuration(new DateTime(2019, 8, 28)).Should().Be("3 months");
new DateTime(2019, 5, 28).ToFriendlyDuration(new DateTime(2019, 8, 31)).Should().Be("3 months, 3 days");
new DateTime(2019, 5, 1).ToFriendlyDuration(new DateTime(2019, 5, 31)).Should().Be("30 days");
new DateTime(2010, 5, 28).ToFriendlyDuration(new DateTime(2020, 8, 28)).Should().Be("10 years, 3 months");
new DateTime(2010, 5, 28).ToFriendlyDuration(new DateTime(2020, 5, 29)).Should().Be("10 years, 1 day");
}
Implementation;
private class TermAndValue
{
public TermAndValue(string singular, string plural, int value)
{
Singular = singular;
Plural = plural;
Value = value;
}
public string Singular { get; }
public string Plural { get; }
public int Value { get; }
public string Term => Value > 1 ? Plural : Singular;
}
public static string ToFriendlyDuration(this DateTime value, DateTime? endDate, int maxNrOfElements = 2)
{
if (!endDate.HasValue)
return "Until further notice";
var extendedTimeSpan = new TimeSpanWithYearAndMonth(value, endDate.Value);
maxNrOfElements = Math.Max(Math.Min(maxNrOfElements, 5), 1);
var termsAndValues = new[]
{
new TermAndValue("year", "years", extendedTimeSpan.Years),
new TermAndValue("month", "months", extendedTimeSpan.Months),
new TermAndValue("day", "days", extendedTimeSpan.Days),
new TermAndValue("hour", "hours", extendedTimeSpan.Hours),
new TermAndValue("minute", "minutes", extendedTimeSpan.Minutes)
};
var parts = termsAndValues.Where(i => i.Value != 0).Take(maxNrOfElements);
return string.Join(", ", parts.Select(p => $"{p.Value} {p.Term}"));
}
internal class TimeSpanWithYearAndMonth
{
internal TimeSpanWithYearAndMonth(DateTime startDate, DateTime endDate)
{
var span = endDate - startDate;
Months = 12 * (endDate.Year - startDate.Year) + (endDate.Month - startDate.Month);
Years = Months / 12;
Months -= Years * 12;
if (Months == 0 && Years == 0)
{
Days = span.Days;
}
else
{
var startDateExceptYearsAndMonths = startDate.AddYears(Years);
startDateExceptYearsAndMonths = startDateExceptYearsAndMonths.AddMonths(Months);
Days = (endDate - startDateExceptYearsAndMonths).Days;
}
Hours = span.Hours;
Minutes = span.Minutes;
}
public int Minutes { get; }
public int Hours { get; }
public int Days { get; }
public int Years { get; }
public int Months { get; }
}
I know this is old, but I wanted to answer with a great nuget package.
Install-Package Humanizer
https://www.nuget.org/packages/Humanizer
https://github.com/MehdiK/Humanizer
Example from their readme.md
TimeSpan.FromMilliseconds(1299630020).Humanize(4) => "2 weeks, 1 day, 1 hour, 30 seconds"
#ian-becker Needs the credit
The TimeSpan object has Days, Hours, Minutes, and Seconds properties on it, so it wouldn't be too hard to make a snippet that formats those values to a friendly string.
Unfortunately Days is the largest value. Anything longer than that and you'll have to start worrying about days in a month for every year...etc. You're better off stopping at days in my opinion (the added effort doesn't seem worth the gain).
UPDATE
...I figured I'd bring this up from my own comment:
Understandable, but is "This document expires in 10 years, 3 months, 21 days, 2 hours, and 30 minutes" really any more helpful or less silly? If it were up to me, since neither representation seems very useful...I'd leave off the timespan for expiry until the date got reasonably close (30 or 60 days maybe if you're worried about getting the document updated). Seems a much better UX choice to me.
It probably won't do everything you are looking for, but in v4 Microsoft will be implementing IFormattable on TimeSpan.
To format any period longer than 1 day (i.e. month/year/decade etc.) a Timespan object is not enough.
Suppose your timespan is 35 days, then from Apr 1 you would get one month and five days, whereas from Dec 1 you would get one month and four days.
Yes! I needed the same so many times then now I create my onw package and published it in Nuget. You are welcome to use it.
The package name is EstecheAssemblies
It's easy to implement:
using EstecheAssemblies;
var date = new DateTime("2019-08-08 01:03:21");
var text = date.ToFriendlyTimeSpan();
See how-do-i-calculate-relative-time, asked (by user number 1) a year ago when SO was young and not public.
It was (probably) the basis for the current age display for SO questions and answers.