Given a specific DateTime value, how do I display relative time, like:
2 hours ago3 days agoa month ago
Given a specific DateTime value, how do I display relative time, like:
2 hours ago3 days agoa month ago/**
* {@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";
}
}
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";
}
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
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.
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'.
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.
// 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();
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;
}
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
.ToNaturalLanguage() method. To do so you should add an additional extension method which rounds the TimeSpan as you desire and then call ToNaturalLanguage().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));
}
}
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;
}
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;
}
"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.