1655

Given a specific DateTime value, how do I display relative time, like:

  • 2 hours ago
  • 3 days ago
  • a month ago
5

42 Answers 42

1
2
4
/** 
 * {@code date1} has to be earlier than {@code date2}.
 */
public static String relativize(Date date1, Date date2) {
    assert date2.getTime() >= date1.getTime();

    long duration = date2.getTime() - date1.getTime();
    long converted;

    if ((converted = TimeUnit.MILLISECONDS.toDays(duration)) > 0) {
        return String.format("%d %s ago", converted, converted == 1 ? "day" : "days");
    } else if ((converted = TimeUnit.MILLISECONDS.toHours(duration)) > 0) {
        return String.format("%d %s ago", converted, converted == 1 ? "hour" : "hours");
    } else if ((converted = TimeUnit.MILLISECONDS.toMinutes(duration)) > 0) {
        return String.format("%d %s ago", converted, converted == 1 ? "minute" : "minutes");
    } else if ((converted = TimeUnit.MILLISECONDS.toSeconds(duration)) > 0) {
        return String.format("%d %s ago", converted, converted == 1 ? "second" : "seconds");
    } else {
        return "just now";
    }
}
Sign up to request clarification or add additional context in comments.

Comments

4

Turkish localized version of Vincents answer.

    const int SECOND = 1;
    const int MINUTE = 60 * SECOND;
    const int HOUR = 60 * MINUTE;
    const int DAY = 24 * HOUR;
    const int MONTH = 30 * DAY;

    var ts = new TimeSpan(DateTime.UtcNow.Ticks - yourDate.Ticks);
    double delta = Math.Abs(ts.TotalSeconds);

    if (delta < 1 * MINUTE)
        return ts.Seconds + " saniye önce";

    if (delta < 45 * MINUTE)
        return ts.Minutes + " dakika önce";

    if (delta < 24 * HOUR)
        return ts.Hours + " saat önce";

    if (delta < 48 * HOUR)
        return "dün";

    if (delta < 30 * DAY)
        return ts.Days + " gün önce";

    if (delta < 12 * MONTH)
    {
        int months = Convert.ToInt32(Math.Floor((double)ts.Days / 30));
        return months + " ay önce";
    }
    else
    {
        int years = Convert.ToInt32(Math.Floor((double)ts.Days / 365));
        return years + " yıl önce";
    }

Comments

4

A "one-liner" using deconstruction and Linq to get "n [biggest unit of time] ago" :

TimeSpan timeSpan = DateTime.Now - new DateTime(1234, 5, 6, 7, 8, 9);

(string unit, int value) = new Dictionary<string, int>
{
    {"year(s)", (int)(timeSpan.TotalDays / 365.25)}, //https://en.wikipedia.org/wiki/Year#Intercalation
    {"month(s)", (int)(timeSpan.TotalDays / 29.53)}, //https://en.wikipedia.org/wiki/Month
    {"day(s)", (int)timeSpan.TotalDays},
    {"hour(s)", (int)timeSpan.TotalHours},
    {"minute(s)", (int)timeSpan.TotalMinutes},
    {"second(s)", (int)timeSpan.TotalSeconds},
    {"millisecond(s)", (int)timeSpan.TotalMilliseconds}
}.First(kvp => kvp.Value > 0);

Console.WriteLine($"{value} {unit} ago");

You get 786 year(s) ago

With the current year and month, like

TimeSpan timeSpan = DateTime.Now - new DateTime(2020, 12, 6, 7, 8, 9);

you get 4 day(s) ago

With the actual date, like

TimeSpan timeSpan = DateTime.Now - DateTime.Now.Date;

you get 9 hour(s) ago

Comments

3
public string getRelativeDateTime(DateTime date)
{
    TimeSpan ts = DateTime.Now - date;
    if (ts.TotalMinutes < 1)//seconds ago
        return "just now";
    if (ts.TotalHours < 1)//min ago
        return (int)ts.TotalMinutes == 1 ? "1 Minute ago" : (int)ts.TotalMinutes + " Minutes ago";
    if (ts.TotalDays < 1)//hours ago
        return (int)ts.TotalHours == 1 ? "1 Hour ago" : (int)ts.TotalHours + " Hours ago";
    if (ts.TotalDays < 7)//days ago
        return (int)ts.TotalDays == 1 ? "1 Day ago" : (int)ts.TotalDays + " Days ago";
    if (ts.TotalDays < 30.4368)//weeks ago
        return (int)(ts.TotalDays / 7) == 1 ? "1 Week ago" : (int)(ts.TotalDays / 7) + " Weeks ago";
    if (ts.TotalDays < 365.242)//months ago
        return (int)(ts.TotalDays / 30.4368) == 1 ? "1 Month ago" : (int)(ts.TotalDays / 30.4368) + " Months ago";
    //years ago
    return (int)(ts.TotalDays / 365.242) == 1 ? "1 Year ago" : (int)(ts.TotalDays / 365.242) + " Years ago";
}

Conversion values for days in a month and year were taken from Google.

3 Comments

Instead of casting TimeSpan.TotalMinutes etc to an integer why not use the TimeSpan.Minutes property? learn.microsoft.com/en-us/dotnet/api/…
They not the same.
Yes but the difference is they only show the number of minutes etc which is the same as this implementation is trying to achieve
3

Surely an easy fix to get rid of the '1 hours ago' problem would be to increase the window that 'an hour ago' is valid for. Change

if (delta < 5400) // 90 * 60
{
    return "an hour ago";
}

into

if (delta < 7200) // 120 * 60
{
    return "an hour ago";
}

This means that something that occurred 110 minutes ago will read as 'an hour ago' - this may not be perfect, but I'd say it is better than the current situation of '1 hours ago'.

Comments

3

In a way you do your DateTime function over calculating relative time by either seconds to years, try something like this:

using System;

public class Program {
    public static string getRelativeTime(DateTime past) {
        DateTime now = DateTime.Today;
        string rt = "";
        int time;
        string statement = "";
        if (past.Second >= now.Second) {
            if (past.Second - now.Second == 1) {
                rt = "second ago";
            }
            rt = "seconds ago";
            time = past.Second - now.Second;
            statement = "" + time;
            return (statement + rt);
        }
        if (past.Minute >= now.Minute) {
            if (past.Second - now.Second == 1) {
                rt = "second ago";
            } else {
                rt = "minutes ago";
            }
            time = past.Minute - now.Minute;
            statement = "" + time;
            return (statement + rt);
        }
        // This process will go on until years
    }
    public static void Main() {
        DateTime before = new DateTime(1995, 8, 24);
        string date = getRelativeTime(before);
        Console.WriteLine("Windows 95 was {0}.", date);
    }
}

Not exactly working but if you modify and debug it a bit, it will likely do the job.

Comments

2
// Calculate total days in current year
int daysInYear;

for (var i = 1; i <= 12; i++)
    daysInYear += DateTime.DaysInMonth(DateTime.Now.Year, i);

// Past date
DateTime dateToCompare = DateTime.Now.Subtract(TimeSpan.FromMinutes(582));

// Calculate difference between current date and past date
double diff = (DateTime.Now - dateToCompare).TotalMilliseconds;

TimeSpan ts = TimeSpan.FromMilliseconds(diff);

var years = ts.TotalDays / daysInYear; // Years
var months = ts.TotalDays / (daysInYear / (double)12); // Months
var weeks = ts.TotalDays / 7; // Weeks
var days = ts.TotalDays; // Days
var hours = ts.TotalHours; // Hours
var minutes = ts.TotalMinutes; // Minutes
var seconds = ts.TotalSeconds; // Seconds

if (years >= 1)
    Console.WriteLine(Math.Round(years, 0) + " year(s) ago");
else if (months >= 1)
    Console.WriteLine(Math.Round(months, 0) + " month(s) ago");
else if (weeks >= 1)
    Console.WriteLine(Math.Round(weeks, 0) + " week(s) ago");
else if (days >= 1)
    Console.WriteLine(Math.Round(days, 0) + " days(s) ago");
else if (hours >= 1)
    Console.WriteLine(Math.Round(hours, 0) + " hour(s) ago");
else if (minutes >= 1)
    Console.WriteLine(Math.Round(minutes, 0) + " minute(s) ago");
else if (seconds >= 1)
    Console.WriteLine(Math.Round(seconds, 0) + " second(s) ago");

Console.ReadLine();

Comments

1

This is my function, works like a charm :)

public static string RelativeDate(DateTime theDate)
{
   var span = DateTime.Now - theDate;
   if (span.Days > 365)
   {
      var years = (span.Days / 365);
      if (span.Days % 365 != 0)
         years += 1;
      return $"about {years} {(years == 1 ? "year" : "years")} ago";
   }
   if (span.Days > 30)
   {
      var months = (span.Days / 30);
      if (span.Days % 31 != 0)
         months += 1;
      return $"about {months} {(months == 1 ? "month" : "months")} ago";
   }
   if (span.Days > 0)
      return $"about {span.Days} {(span.Days == 1 ? "day" : "days")} ago";
   if (span.Hours > 0)
      return $"about {span.Hours} {(span.Hours == 1 ? "hour" : "hours")} ago";
   if (span.Minutes > 0)
      return $"about {span.Minutes} {(span.Minutes == 1 ? "minute" : "minutes")} ago";
   if (span.Seconds > 5)
      return $"about {span.Seconds} seconds ago";

   return span.Seconds <= 5 ? "about 5 seconds ago" : string.Empty;
}

1 Comment

IMO, you need to explain why your solution is significantly different from, and better than, the other 70-odd answers (some of which are deleted so you don't see them).
0

The accepted answer by Vincent makes quite a few arbitrary decisions. Why is 45 minutes rounded up to an hour while 45 seconds is not rounded up to a minute? It has an increased level of cyclomatic complexity within the years and month calculations that makes it more complex to follow the logic. It makes the assumption that the TimeSpan is relative to the past (2 days ago) when it could very well be in the future (2 days until). It defines unnecessary constants instead of using TimeSpan.TicksPerSecond etc.

This implementation resolves the above and updates the syntax to use switch expressions and relational patterns

/// <summary>
/// Convert a <see cref="TimeSpan"/> to a natural language representation.
/// </summary>
/// <example>
/// <code>
/// TimeSpan.FromSeconds(10).ToNaturalLanguage();
/// // 10 seconds
/// </code>
/// </example>
public static string ToNaturalLanguage(this TimeSpan @this)
{
    const int daysInWeek = 7;
    const int daysInMonth = 30;
    const int daysInYear = 365;
    const long threshold = 100 * TimeSpan.TicksPerMillisecond;
    @this = @this.TotalSeconds < 0
        ? TimeSpan.FromSeconds(@this.TotalSeconds * -1)
        : @this;
    return (@this.Ticks + threshold) switch
    {
        < 2 * TimeSpan.TicksPerSecond => "a second",
        < 1 * TimeSpan.TicksPerMinute => @this.Seconds + " seconds",
        < 2 * TimeSpan.TicksPerMinute => "a minute",
        < 1 * TimeSpan.TicksPerHour => @this.Minutes + " minutes",
        < 2 * TimeSpan.TicksPerHour => "an hour",
        < 1 * TimeSpan.TicksPerDay => @this.Hours + " hours",
        < 2 * TimeSpan.TicksPerDay => "a day",
        < 1 * daysInWeek * TimeSpan.TicksPerDay => @this.Days + " days",
        < 2 * daysInWeek * TimeSpan.TicksPerDay => "a week",
        < 1 * daysInMonth * TimeSpan.TicksPerDay => (@this.Days / daysInWeek).ToString("F0") + " weeks",
        < 2 * daysInMonth * TimeSpan.TicksPerDay => "a month",
        < 1 * daysInYear * TimeSpan.TicksPerDay => (@this.Days / daysInMonth).ToString("F0") + " months",
        < 2 * daysInYear * TimeSpan.TicksPerDay => "a year",
        _ => (@this.Days / daysInYear).ToString("F0") + " years"
    };
}

/// <summary>
/// Convert a <see cref="DateTime"/> to a natural language representation.
/// </summary>
/// <example>
/// <code>
/// (DateTime.Now - TimeSpan.FromSeconds(10)).ToNaturalLanguage()
/// // 10 seconds ago
/// </code>
/// </example>
public static string ToNaturalLanguage(this DateTime @this)
{
    TimeSpan timeSpan = @this - DateTime.Now;
    return timeSpan.TotalSeconds switch
    {
        >= 1 => timeSpan.ToNaturalLanguage() + " until",
        <= -1 => timeSpan.ToNaturalLanguage() + " ago",
        _ => "now",
    };
}

You can test it with NUnit as follows:

[TestCase("a second", 0)]
[TestCase("a second", 1)]
[TestCase("2 seconds", 2)]
[TestCase("a minute", 0, 1)]
[TestCase("5 minutes", 0, 5)]
[TestCase("an hour", 0, 0, 1)]
[TestCase("2 hours", 0, 0, 2)]
[TestCase("a day", 0, 0, 24)]
[TestCase("a day", 0, 0, 0, 1)]
[TestCase("6 days", 0, 0, 0, 6)]
[TestCase("a week", 0, 0, 0, 7)]
[TestCase("4 weeks", 0, 0, 0, 29)]
[TestCase("a month", 0, 0, 0, 30)]
[TestCase("6 months", 0, 0, 0, 6 * 30)]
[TestCase("a year", 0, 0, 0, 365)]
[TestCase("68 years", int.MaxValue)]
public void NaturalLanguageHelpers_TimeSpan(
    string expected,
    int seconds,
    int minutes = 0,
    int hours = 0,
    int days = 0
)
{
    // Arrange
    TimeSpan timeSpan = new(days, hours, minutes, seconds);

    // Act
    string result = timeSpan.ToNaturalLanguage();

    // Assert
    Assert.That(result, Is.EqualTo(expected));
}

[TestCase("now", 0)]
[TestCase("10 minutes ago", 0, -10)]
[TestCase("10 minutes until", 10, 10)]
[TestCase("68 years until", int.MaxValue)]
[TestCase("68 years ago", int.MinValue)]
public void NaturalLanguageHelpers_DateTime(
    string expected,
    int seconds,
    int minutes = 0,
    int hours = 0,
    int days = 0
)
{
    // Arrange
    TimeSpan timeSpan = new(days, hours, minutes, seconds);
    DateTime now = DateTime.Now;
    DateTime dateTime = now + timeSpan;

    // Act
    string result = dateTime.ToNaturalLanguage();

    // Assert
    Assert.That(result, Is.EqualTo(expected));
}

Or as a gist: https://gist.github.com/StudioLE/2dd394e3f792e79adc927ede274df56e

1 Comment

Variants where 45 minutes is rounded to an hour, or use "yesterday" instead of "a day ago" are specific implementation decisions and therefore should be separate to the .ToNaturalLanguage() method. To do so you should add an additional extension method which rounds the TimeSpan as you desire and then call ToNaturalLanguage().
0

This is a C# extension for '... ago' and '... left' scenarios:

public static class DateTimeExtensions
{
    public static string? ToAgoOrLeftText(this DateTime? moment, DateTime? to, string ago = "ago", string left = "left")
    {
        return moment?.ToAgoOrLeftText(to, ago, left);
    }

    public static string? ToAgoOrLeftText(this DateTime moment, DateTime? to, string ago = "ago", string left = "left")
    {
        if (!to.HasValue) return null;

        const int second = 1;
        const int minute = 60 * second;
        const int hour = 60 * minute;
        const int day = 24 * hour;
        const int month = 30 * day;

        var past = to.Value < moment;
        var when = past ? ago : left;

        var ts = new TimeSpan(moment.Ticks - to.Value.Ticks);
        double deltaInSeconds = Math.Abs(ts.TotalSeconds);

        int seconds = Math.Abs(ts.Seconds);
        int minutes = Math.Abs(ts.Minutes);
        int hours = Math.Abs(ts.Hours);
        int days = Math.Abs(ts.Days);
        int months =  Math.Abs(Convert.ToInt32(Math.Floor((double)ts.Days / 30)));
        int years = Math.Abs(Convert.ToInt32(Math.Floor((double)ts.Days / 365)));

        return deltaInSeconds switch
        {
            < 1 * minute => seconds == 0 ? "now" : seconds == 1 ? $"one second {when}" : $"{seconds} seconds {when}",
            < 2 * minute => $"a minute {when}",
            < 45 * minute => $"{minutes} minutes {when}",
            < 90 * minute => $"an hour {when}",
            < 24 * hour => $"{hours} hours {when}",
            < 48 * hour => past ? "yesterday" : $"a day {left}",
            < 30 * day => $"{days} days {when}",
            < 12 * month => months <= 1 ? $"one month {when}" : $"{months} months {when}",
            _ => years <= 1 ? $"one year {when}" : $"{years} years {when}"
        };
    }
}

And the test to demo:

public class DateTimeExtensionsTests
{
    [Fact]
    public void ToAgoOrLeftTextTest()
    {
        var moment = new DateTime(2023, 11, 28, 15, 30, 00);

        Assert.Equal("now", moment.ToAgoOrLeftText(moment));

        Assert.Equal("one second left", moment.ToAgoOrLeftText(new DateTime(2023, 11, 28, 15, 30, 01)));
        Assert.Equal("one second ago", moment.ToAgoOrLeftText(new DateTime(2023, 11, 28, 15, 29, 59)));

        Assert.Equal("25 seconds left", moment.ToAgoOrLeftText(new DateTime(2023, 11, 28, 15, 30, 25)));
        Assert.Equal("23 seconds ago", moment.ToAgoOrLeftText(new DateTime(2023, 11, 28, 15, 29, 37)));

        Assert.Equal("a minute left", moment.ToAgoOrLeftText(new DateTime(2023, 11, 28, 15, 31, 01)));
        Assert.Equal("a minute ago", moment.ToAgoOrLeftText(new DateTime(2023, 11, 28, 15, 28, 30)));

        Assert.Equal("7 minutes left to claim", moment.ToAgoOrLeftText(new DateTime(2023, 11, 28, 15, 37, 01), left: "left to claim"));
        Assert.Equal("4 minutes since that happen", moment.ToAgoOrLeftText(new DateTime(2023, 11, 28, 15, 25, 30), ago: "since that happen"));

        Assert.Equal("an hour left", moment.ToAgoOrLeftText(new DateTime(2023, 11, 28, 16, 45, 25)));
        Assert.Equal("an hour ago", moment.ToAgoOrLeftText(new DateTime(2023, 11, 28, 14, 02, 59)));

        Assert.Equal("2 hours left", moment.ToAgoOrLeftText(new DateTime(2023, 11, 28, 18, 15, 00)));
        Assert.Equal("5 hours ago", moment.ToAgoOrLeftText(new DateTime(2023, 11, 28, 10, 02, 30)));

        Assert.Equal("yesterday", moment.ToAgoOrLeftText(new DateTime(2023, 11, 27, 14, 15, 00)));
        Assert.Equal("a day left", moment.ToAgoOrLeftText(new DateTime(2023, 11, 30, 10, 02, 30)));

        Assert.Equal("29 days ago", moment.ToAgoOrLeftText(new DateTime(2023, 10, 30, 14, 15, 00)));
        Assert.Equal("16 days left", moment.ToAgoOrLeftText(new DateTime(2023, 12, 15, 10, 02, 30)));

        Assert.Equal("one month ago", moment.ToAgoOrLeftText(new DateTime(2023, 10, 26, 14, 15, 00)));
        Assert.Equal("one month left", moment.ToAgoOrLeftText(new DateTime(2023, 12, 29, 10, 02, 30)));

        Assert.Equal("5 months ago", moment.ToAgoOrLeftText(new DateTime(2023, 6, 30, 14, 15, 00)));
        Assert.Equal("6 months left", moment.ToAgoOrLeftText(new DateTime(2024, 05, 15, 10, 02, 30)));

        Assert.Equal("23 years ago", moment.ToAgoOrLeftText(new DateTime(2000, 6, 30, 14, 15, 00)));
        Assert.Equal("12 years left", moment.ToAgoOrLeftText(new DateTime(2035, 05, 15, 10, 02, 30)));
    }

    [Fact]
    public void ToAgoOrLeftTextNullableTest()
    {
        DateTime? moment = new DateTime(2023, 11, 28, 15, 30, 00);

        Assert.Null(moment.ToAgoOrLeftText(null));
        Assert.Null(moment.Value.ToAgoOrLeftText(null));

        moment = null;

        Assert.Null(moment.ToAgoOrLeftText(new DateTime(2023, 11, 28, 15, 29, 59)));
        Assert.Null(moment.ToAgoOrLeftText(null));
    }
}

Comments

-1

My way is much more simpler. You can tweak with the return strings as you want

    public static string TimeLeft(DateTime utcDate)
    {
        TimeSpan timeLeft = DateTime.UtcNow - utcDate;
        string timeLeftString = "";
        if (timeLeft.Days > 0)
        {
            timeLeftString += timeLeft.Days == 1 ? timeLeft.Days + " day" : timeLeft.Days + " days";
        }
        else if (timeLeft.Hours > 0)
        {
            timeLeftString += timeLeft.Hours == 1 ? timeLeft.Hours + " hour" : timeLeft.Hours + " hours";
        }
        else
        {
            timeLeftString += timeLeft.Minutes == 1 ? timeLeft.Minutes+" minute" : timeLeft.Minutes + " minutes";
        }
        return timeLeftString;
    }

Comments

-1

Simple and 100% working solution.

Handling ago and future times as well.. just in case

        public string GetTimeSince(DateTime postDate)
    {
        string message = "";
        DateTime currentDate = DateTime.Now;
        TimeSpan timegap = currentDate - postDate;

     
        if (timegap.Days > 365)
        {
            message = string.Format(L("Ago") + " {0} " + L("Years"), (((timegap.Days) / 30) / 12));                
        }
        else if (timegap.Days > 30)
        {
            message = string.Format(L("Ago") + " {0} " + L("Months"), timegap.Days/30);                
        }
        else if (timegap.Days > 0)
        {
            message = string.Format(L("Ago") + " {0} " + L("Days"), timegap.Days);
        }           
        else if (timegap.Hours > 0)
        {
            message = string.Format(L("Ago") + " {0} " + L("Hours"), timegap.Hours);
        }           
        else if (timegap.Minutes > 0)
        {
            message = string.Format(L("Ago") + " {0} " + L("Minutes"), timegap.Minutes);
        }
        else if (timegap.Seconds > 0)
        {
            message = string.Format(L("Ago") + " {0} " + L("Seconds"), timegap.Seconds);
        }

        // let's handle future times..just in case       
        else if (timegap.Days < -365)
        {
            message = string.Format(L("In") + " {0} " + L("Years"), (((Math.Abs(timegap.Days)) / 30) / 12));                
        }
        else if (timegap.Days < -30)
        {
            message = string.Format(L("In") + " {0} " + L("Months"), ((Math.Abs(timegap.Days)) / 30));                
        }
        else if (timegap.Days < 0)
        {
            message = string.Format(L("In") + " {0} " + L("Days"), Math.Abs(timegap.Days));                
        }           
      
        else if (timegap.Hours < 0)
        {
            message = string.Format(L("In") + " {0} " + L("Hours"), Math.Abs(timegap.Hours));                
        }
        else if (timegap.Minutes < 0)
        {
            message = string.Format(L("In") + " {0} " + L("Minutes"), Math.Abs(timegap.Minutes));                
        }
        else if (timegap.Seconds < 0)
        {
            message = string.Format(L("In") + " {0} " + L("Seconds"), Math.Abs(timegap.Seconds));                
        }


        else
        {
            message = "a bit";
        }

        return message;
    }

5 Comments

IMO, you need to explain why your solution is significantly different from, and better than, the other 70-odd answers (some of which are deleted so you don't see them).
@JonathanLeffler If you see with the open eyes you will realized that there is an addition in this answer which is to handle the future times as well just in case. For example: If the registration is valid it will say. In 6 months it will going to expire If the registration is expired it wil say. 6 Months ago it expired
No other answer had cover that. Hope you got it
@jhonny-d-cano-leftware Check this one for future time handling
The repetition is rather obvious and undesirable. Surely there is a way to determine if the time gap is negative and start the message with "In" instead of "Ago" (though it might be better to have "In 5 minutes" or "5 minutes ago" — but that too can be handled without the repetition). You could then negate the time span (or calculate the time span with the DateTime values in reverse order), and then build the string with one set of conditions.
1
2

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.