28

I have a class with 2 date properties: FirstDay and LastDay. LastDay is nullable. I would like to generate a string in the format of "x year(s) y day(s)". If the total years are less than 1, I would like to omit the year section. If the total days are less than 1, I would like to omit the day section. If either years or days are 0, they should say "day/year", rather than "days/years" respectively.

Examples:
2.2 years:             "2 years 73 days"
1.002738 years:   "1 year 1 day"
0.2 years:             "73 days"
2 years:                "2 years"

What I have works, but it is long:

private const decimal DaysInAYear = 365.242M;

public string LengthInYearsAndDays
{
    get
    {
        var lastDay = this.LastDay ?? DateTime.Today;
        var lengthValue = lastDay - this.FirstDay;

        var builder = new StringBuilder();

        var totalDays = (decimal)lengthValue.TotalDays;
        var totalYears = totalDays / DaysInAYear;
        var years = (int)Math.Floor(totalYears);

        totalDays -= (years * DaysInAYear);
        var days = (int)Math.Floor(totalDays);

        Func<int, string> sIfPlural = value =>
            value > 1 ? "s" : string.Empty;

        if (years > 0)
        {
            builder.AppendFormat(
                CultureInfo.InvariantCulture,
                "{0} year{1}",
                years,
                sIfPlural(years));

            if (days > 0)
            {
                builder.Append(" ");
            }
        }

        if (days > 0)
        {
            builder.AppendFormat(
                CultureInfo.InvariantCulture,
                "{0} day{1}",
                days,
                sIfPlural(days));
        }

        var length = builder.ToString();
        return length;
    }
}

Is there a more concise way of doing this (but still readable)?

Dan
  • 9,717
  • 4
  • 47
  • 65

6 Answers6

47

A TimeSpan doesn't have a sensible concept of "years" because it depends on the start and end point. (Months is similar - how many months are there in 29 days? Well, it depends...)

To give a shameless plug, my Noda Time project makes this really simple though:

using System;
using NodaTime;

public class Test
{
    static void Main(string[] args)
    {
        LocalDate start = new LocalDate(2010, 6, 19);
        LocalDate end = new LocalDate(2013, 4, 11);
        Period period = Period.Between(start, end,
                                       PeriodUnits.Years | PeriodUnits.Days);

        Console.WriteLine("Between {0} and {1} are {2} years and {3} days",
                          start, end, period.Years, period.Days);
    }
}

Output:

Between 19 June 2010 and 11 April 2013 are 2 years and 296 days
Herohtar
  • 5,347
  • 4
  • 31
  • 41
Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • 14
    And you can definitely trust Jon's expertise on [times](http://stackoverflow.com/a/6841479/211627) and [dates](http://msmvps.com/blogs/jon_skeet/archive/2010/12/01/the-joys-of-date-time-arithmetic.aspx) – JDB Apr 11 '13 at 20:23
  • 8
    ugh, extra third party library - but library by Jon Skeet is pretty convincing. – tofutim Mar 18 '16 at 01:21
  • This doesn't even make sense. years can be calculated by a start and end point. It has nothing to do with it not making sense, it has to do with no support from microsoft. DateTimeOffset could have been used, but they made the minimum constant value to be 0001/01/01 instead of correctly allowing 0000/00/00 so people could calculate differences. – Shadowblitz16 Sep 10 '21 at 01:14
  • @Shadowblitz16: "years can be calculated by a start and end point" - sure, but that's not what TimeSpan represents. And no, I don't think allowing 0000/00/00 would be "correct" or helpful. – Jon Skeet Sep 10 '21 at 01:23
  • @Jon Skeet I disagree. Its actually helpful when getting a difference between the two dates. If the day, month or year has no difference it would be zero, but C# doesn't allow for that so I have to calculate them separately manually. – Shadowblitz16 Sep 10 '21 at 20:35
  • 1
    @Shadowblitz16: No, if you've got two *valid* dates, you don't need an "extra" date that's completely invalid (which 0000/00/00 is). It's unclear to me *exactly* what you want to do, but I'd be very surprised if "let's introduce an invalid date into the system" is a useful way of achieving it. (Bear in mind that I've spent a *lot* of time considering date/time APIs.) If you want to pursue this further, I suggest you ask a new question with more details of what you're really trying to achieve. – Jon Skeet Sep 11 '21 at 10:29
8
public string GetAgeText(DateTime birthDate)
{
        const double ApproxDaysPerMonth = 30.4375;
        const double ApproxDaysPerYear = 365.25;

        /*
        The above are the average days per month/year over a normal 4 year period
        We use these approximations as they are more accurate for the next century or so
        After that you may want to switch over to these 400 year approximations

           ApproxDaysPerMonth = 30.436875
           ApproxDaysPerYear  = 365.2425 

          How to get theese numbers:
            The are 365 days in a year, unless it is a leepyear.
            Leepyear is every forth year if Year % 4 = 0
            unless year % 100 == 1
            unless if year % 400 == 0 then it is a leep year.

            This gives us 97 leep years in 400 years. 
            So 400 * 365 + 97 = 146097 days.
            146097 / 400      = 365.2425
            146097 / 400 / 12 = 30,436875

        Due to the nature of the leap year calculation, on this side of the year 2100
        you can assume every 4th year is a leap year and use the other approximatiotions

        */
    //Calculate the span in days
    int iDays = (DateTime.Now - birthDate).Days;

    //Calculate years as an integer division
    int iYear = (int)(iDays / ApproxDaysPerYear);

    //Decrease remaing days
    iDays -= (int)(iYear * ApproxDaysPerYear);

    //Calculate months as an integer division
    int iMonths = (int)(iDays / ApproxDaysPerMonth);

    //Decrease remaing days
    iDays -= (int)(iMonths * ApproxDaysPerMonth);

    //Return the result as an string   
    return string.Format("{0} years, {1} months, {2} days", iYear, iMonths, iDays);
}
Yuck
  • 49,664
  • 13
  • 105
  • 135
Jens Borrisholt
  • 101
  • 1
  • 2
  • Modified your last line to `return (iYear > 0 ? $"{iYear} years, " : "") + (iMonths > 0 ? $"{iMonths} months, " : "") + (iDays > 0 ? $"{iDays} days" : "").Trim(' ', ',');` to cleanup the output. Great simple solution – Thymine Jun 04 '19 at 22:42
  • Thanks for the math. I have used this to create a `ToHumanReadableString` function. https://stackoverflow.com/a/75325021/1847143 – TheBigNeo Apr 11 '23 at 08:46
0

I wouldn't do this with a TimeSpan. Date math gets tricky as soon as you go beyond days because the number of days in a month and days in a year is no longer constant. It's likely why TimeSpan does not contain properties for Years and Months. I would instead determine the number of years/months/days, etc between the two DateTime values and display the results accordingly.

D Stanley
  • 149,601
  • 11
  • 178
  • 240
  • 1
    That said, it looks like the OP has settled on a sufficient compromise (for his purposes): `365.242M` days per year. – JDB Apr 11 '13 at 20:15
  • 1
    There aren't 365.242 days in a year. Some years have 365 days, some have 366. There are 365.242 in an _average_ year, which won't work if you're comparing two specific dates. If all that was available was the number of days (which is the bast `TimeSpan` can do), then it would be a decent _estimate_ but could be off by one day in certain cases. – D Stanley Apr 11 '13 at 20:25
  • 1
    I agree with you, I'm just sayin' for a non-public, personal project, precision may (legitimately) take a backseat to convenience. – JDB Apr 11 '13 at 20:27
  • How do we know this isn't being used to calculate the interest on the national debt? :) – D Stanley Apr 11 '13 at 20:28
  • 1
    I was actually using this to calculate the interest on the national debt. – Dan Apr 11 '13 at 20:30
  • 2
    Cool! Can you mail me the check for the rounding difference? ;) – D Stanley Apr 11 '13 at 20:32
  • @DStanley - Still looks like a better estimate then some of the debt interest calculators I've seen. ;) – JDB Apr 11 '13 at 20:39
0

I think this should work:

public static int DiffYears(DateTime dateValue1, DateTime dateValue2)
{
    var intToCompare1 = Convert.ToInt32(dateValue1.ToString("yyyyMMdd"));
    var intToCompare2 = Convert.ToInt32(dateValue2.ToString("yyyyMMdd"));
    return (intToCompare2 - intToCompare1) / 10000;
}
Clak
  • 58
  • 5
  • You're rendering the month and day here for the conversion to `int`, but then dividing by flat ten thousand - casted to an Int. This is almost equivalent to doing `dateValue1.Year - dateValue2.year` in terms of accuracy. The result of this would be a single number indicating how years have completely past regardless of any possible 354 trailing days. Semantically, this is also very dubious. – Kana Ki Nov 22 '16 at 19:24
0
Public Function TimeYMDBetween(StartDate As DateTime, EndDate As DateTime) As String
    Dim Years As Integer = EndDate.Year - StartDate.Year
    Dim Months As Integer = EndDate.Month - StartDate.Month
    Dim Days As Integer = EndDate.Day - StartDate.Day
    Dim DaysLastMonth As Integer

    'figure out how many days were in last month
    If EndDate.Month = 1 Then
        DaysLastMonth = DateTime.DaysInMonth(EndDate.Year - 1, 12)
    Else
        DaysLastMonth = DateTime.DaysInMonth(EndDate.Year, EndDate.Month - 1)
    End If

    'adjust for negative days
    If Days < 0 Then
        Months = Months - 1
        Days = Days + DaysLastMonth 'borrowing from last month
    End If

    'adjust for negative Months
    If Months < 0 Then 'startdate hasn't happend this year yet
        Years = Years - 1
        Months = Months + 12
    End If

    Return Years.ToString() + " Years, " + Months.ToString() + " Months and " + Days.ToString() + " Days"

End Function
Mark
  • 1
  • 1
0

I needed to do this for Core 3. NodaTime seems to have a dependancy on Framework 4.7.2. I wrote the below method which seems to work to format a timespan into years, months and days, omitting the parts it doesn't need.

public static string ToYearsMonthsAndDays(this TimeSpan span)
    {
        var result = string.Empty;
        var totalYears = span.Days / 364.25;
        var fullYears = Math.Floor(totalYears);

        var totalMonths = (span.Days - (365.24 * fullYears)) / 30;
        var fullMonths = Math.Floor(totalMonths);

        var totalDays = (span.Days - (365.24 * totalYears) - (30 * fullMonths)) / 30;
        var fullDays = Math.Floor(totalDays);
        var sb = new StringBuilder();
        if (fullYears > 0)
        {
            if (sb.Length > 0)
                sb.Append(", ");
            sb.Append(fullYears + "y");
        }
        if (fullMonths > 0)
        {
            if (sb.Length > 0)
                sb.Append(", ");
            sb.Append(fullMonths + "m");
        }
        if (fullDays > 0)
        {
            if (sb.Length > 0)
                sb.Append(", ");
            sb.Append(fullDays + "d");
        }
        return sb.ToString();
    }
Echilon
  • 10,064
  • 33
  • 131
  • 217