JohnJohnGa
JohnJohnGa

Reputation: 15685

Display relative time in hour, day, month and year

I wrote a function

toBeautyString(epoch) : String

which given a epoch, return a string which will display the relative time from now in hour and minute

For instance:

// epoch: 1346140800 -> Tue, 28 Aug 2012 05:00:00 GMT 
// and now: 1346313600 -> Thu, 30 Aug 2012 08:00:00 GMT
toBeautyString(1346140800) 
-> "2 days and 3 hours ago"

I want now to extend this function to month and year, so it will be able to print:

2 years, 1 month, 3 days and 1 hour ago

Only with epoch without any external libraries. The purpose of this function is to give to the user a better way to visualize the time in the past.

I found this: Calculate relative time in C# but the granularity is not enough.

function toBeautyString(epochNow, epochNow){
    var secDiff = Math.abs(epochNow - epochNow);
    var milliInDay = 1000 * 60 * 60 * 24;
    var milliInHour = 1000 * 60 * 60;

    var nbDays = Math.round(secDiff/milliInDay);
    var nbHour = Math.round(secDiff/milliInHour);

    var relativeHour = (nbDays === 0) ? nbHour : nbHour-(nbDays*24);
    relativeHour %= 24;

    if(nbHour === 0){
        nbDays += 1;
    }else if(nbHour === (nbDays-1)*24){
        nbDays -= 1;
    }

    var dayS = (nbDays > 1) ? "days" : "day";
    var hourS = (relativeHour > 1) ? "hours" : "hour";

    var fullString = "";

    if(nbDays > 0){
        fullString += nbDays + " " + dayS;
        if(relativeHour > 0)
            fullString += " ";
    }

    if(relativeHour > 0){
        fullString += relativeHour + " " + hourS;
    }

    if(epochDate > epochNow){
        return "Will be in " + fullString;
    }else if ((epochDate === epochNow) 
            || (relativeHour === 0 && nbDays === 0)){
        return "Now";
    }else{
        return fullString + " ago";         
    }
}

Upvotes: 7

Views: 2604

Answers (5)

hauntsaninja
hauntsaninja

Reputation: 1019

As exhaustively discussed in other answers, your code can't easily be extended because of the variable month lengths. So one simply can't assume the month to be 30 days.

In order to have a human-readable difference, you must subtract from the human-readable dates.

I'd do it like this (JavaScript, to match the question):

function toBeautyString(then) {

    var nowdate = new Date();
    var thendate = new Date(then * 1000);

    //finding the human-readable components of the date.

    var y = nowdate.getFullYear() - thendate.getFullYear();
    var m = nowdate.getMonth() - thendate.getMonth();
    var d = nowdate.getDate() - thendate.getDate();
    var h = nowdate.getHours() - thendate.getHours();
    var mm = nowdate.getMinutes() - thendate.getMinutes();
    var s = nowdate.getSeconds() - thendate.getSeconds();

    //back to second grade math, now we must now 'borrow'.

    if(s < 0) {
            s += 60;
            mm--;
    }
    if(mm < 0) {
            mm += 60;
            h--;
    }
    if(h < 0) {
            h += 24;
            d--;
    }
    if(d < 0) {

            //here's where we take into account variable month lengths.

            var a = thendate.getMonth();
            var b;
            if(a <= 6) {
                    if(a == 1) b = 28;
                    else if(a % 2 == 0) b = 31;
                    else b = 30;
            }
            else if(b % 2 == 0) b = 30;
            else b = 31;

            d += b;
            m--;
    }
    if(m < 0) {
            m += 12;
            y--;
    }

    //return "y years, m months, d days, h hours, mm minutes and s seconds ago."
}

The code works by subtracting from the human-readable dates (obtained using the in-built javascript commands). The only work left is to ensure that any borrowing over proceeds smoothly. This is easy, except in the case where you're borrowing from the months, because months have variable length.

Say you're subtracting 25th February from 12th April.

Before borrowing takes place, m = 2 and d = -13. Now, when you borrow from m, m = 1, but you need to ensure that d increases by 28, as you are borrowing across February. The final result is 1 month, 15 days ago.

If you were subtracting 25th July from 12th September, the result would be 1 month, 18 days ago.

The only thing the code above does not provide for is leap years. This is easily extendable: you simply need to take into account the year and adjust by the one necessary if you're borrowing over February.

Upvotes: 2

tucuxi
tucuxi

Reputation: 17945

Two functions: one to calculate the difference, and another to show it (inspired by Kevin's answer). Works for all my tests, takes into account month durations, easy to translate, and also works around daylights-saving time.

/**
 * Calculates difference from 'now' to a timestamp, using pretty units
 * (years, months, days, hours, minutes and seconds). 
 * Timestamps in ms, second argument is optional (assumes "now").
 */
function abstractDifference(thenTimestamp, nowTimestamp) {
    var now = nowTimestamp ? new Date(nowTimestamp) : new Date();
    var then = new Date(thenTimestamp);
    var nowTimestamp = Math.round(now.getTime());
    console.log(nowTimestamp, thenTimestamp);

    // -- part 1, in which we figure out the difference in days

    var deltaSeconds = Math.round((nowTimestamp - thenTimestamp)/1000);

    // adjust offset for daylight savings time: 2012/01/14 to 2012/04/14 
    // is '3 months', not 2 months 23 hours (for most earth-bound humans)
    var offsetNow = now.getTimezoneOffset();
    var offsetThen = then.getTimezoneOffset();
    deltaSeconds -= (offsetNow - offsetThen) * 60; 


    // positive integers are easier to work with; and months are sensiteive to +/-
    var inTheFuture = false;
    if (deltaSeconds < 0) {
        inTheFuture = true;
        deltaSeconds = -deltaSeconds;
    }

    var seconds = deltaSeconds % 60;
    var deltaMinutes = Math.floor(deltaSeconds / 60);
    var minutes = deltaMinutes % 60;
    var deltaHours = Math.floor(deltaMinutes / 60); 
    var hours = deltaHours % 24;
    var deltaDays = Math.floor(deltaHours / 24);    
    console.log("delta days: ", deltaDays);           

    // -- part 2, in which months figure prominently

    function daysInMonth(year, month) {
        // excess days automagically wrapped around; see details at
        // http://www.ecma-international.org/publications/standards/Ecma-262.htm
        return 32 - new Date(year, month, 32).getDate();
    }
    var months = 0;
    var currentMonth = now.getMonth();
    var currentYear = now.getFullYear();    
    if ( ! inTheFuture) {
        // 1 month ago means "same day-of-month, last month"
        // it is the length of *last* month that is relevant
        currentMonth --;  
        while (true) {
            if (currentMonth < 0) {
                currentMonth = 11;
                currentYear--;
            }
            var toSubstract = daysInMonth(currentYear, currentMonth);
            if (deltaDays >= toSubstract) {
                deltaDays -= toSubstract;
                months ++;
                currentMonth --;
            } else {
                break;
            }
        }
    } else {
        // in 1 month means "same day-of-month, next month"
        // it is the length of *this* month that is relevant
        while (true) {
            if (currentMonth > 11) {
                currentMonth = 0;
                currentYear++;
            }
            var toSubstract = daysInMonth(currentYear, currentMonth);
            if (deltaDays >= toSubstract) {
                deltaDays -= toSubstract;
                months ++;
                currentMonth ++;
            } else {
                break;
            }
        }   
    }

    var years = Math.floor(months / 12);
    var months = months % 12;

    return {future: inTheFuture, 
        years: years, months: months, days: deltaDays, 
        hours: hours, minutes: minutes, seconds: seconds};
}

/**
 * Returns something like "1 year, 4 days and 1 second ago", or 
 * "in 1 month, 3 hours, 45 minutes and 59 seconds".
 * Second argument is optional.
 */
function prettyDifference(thenTimestamp, nowTimestamp) {
    var o = abstractDifference(thenTimestamp, nowTimestamp);
    var parts = [];
    function pushPart(property, singular, plural) {
        var value = o[property];
        if (value) parts.push("" + value + " " + (value==1?singular:plural));
    }
    // to internationalize, change things here
    var lastSeparator = " and ";
    var futurePrefix = "in ";
    var pastSuffix = " ago";
    var nameOfNow = "now";
    pushPart("years", "year", "years");
    pushPart("months", "month", "months");
    pushPart("days", "day", "days");
    pushPart("hours", "hour", "hours");
    pushPart("minutes", "minute", "minutes");
    pushPart("seconds", "second", "seconds");

    if (parts.length == 0) {
        return nameOfNow;
    }

    var beforeLast = parts.slice(0, -1).join(", ");
    var pendingRelative = parts.length > 1 ? 
        [beforeLast , parts.slice(-1) ].join(lastSeparator) :
        parts[0];
    return o.future ? 
        futurePrefix + pendingRelative : pendingRelative + pastSuffix;
}

Upvotes: 1

user687474
user687474

Reputation:

You can use the class DateDiff from the Time Period Library for .NET to display the relative time:

// ----------------------------------------------------------------------
public void DateDiffSample( DateTime epoch )
{
  DateDiff dateDiff = new DateDiff( DateTime.Now, epoch );
  Console.WriteLine( "{0} ago", dateDiff.GetDescription( 4 ) );
  // > 1 Year 4 Months 12 Days 12 Hours ago
} // DateDiffSample

Upvotes: 2

Hubert Sch&#246;lnast
Hubert Sch&#246;lnast

Reputation: 8517

There can't be such a algorithm!

Fractions of a day (hours, minutes, seconds), even days itselfs, are no problem. The problem is that the length of "a month" is varying between 28 and 31 days.

I give you an example:

Lets say today is 28 Feb 2013 and you want to calculate toBeautyString(28 Jan 2013):

today: 28 Feb 2013
toBeautyString(28 Jan 2013)
expected answer: 1 month ago

Well, that was really no problem. Same day-number, same year, just the month did change.

Now lets calculate toBeautyString(27 Jan 2013) at the same day:

today: 28 Feb 2013
toBeautyString(27 Jan 2013)
expected answer: 1 month and 1 day ago

Thats also simple, isn't it? We wanted the value for the day before and the output says that the duration is one day longer.

Now lets go to bed and keep on working at the next day (1 Mar 2013).
Try this:

today: 1 Mar 2013
toBeautyString(1 Feb 2013)
expected answer: 1 month ago

Well, so simple! Same logic as your first calculation. Only the month did change by 1, so the duration can't be anything else but 1 month.
So, lets calculate the value for the day before:

today: 1 Mar 2013
toBeautyString(31 Jan 2013)
expected answer: 1 month and 1 day ago

Again: the result of the day before must be a duration that is 1 day longer.
Let's try to make the duration 1 day longer:

today: 1 Mar 2013
toBeautyString(30 Jan 2013)
expected answer: 1 month and 2 days ago

and longer:

today: 1 Mar 2013
toBeautyString(29 Jan 2013)
expected answer: 1 month and 3 days ago

and finally:

today: 1 Mar 2013
toBeautyString(28 Jan 2013)
expected answer: 1 month and 4 days ago

Remember this!


No lets repeat the very first calculation we did yesterday. Yesterday we did calculate toBeautyString(28 Jan 2013) and the result was 1 month ago. Today is one day later. If we calculate toBeautyString(28 Jan 2013) today, the result should show us a duration that is one day longer:

today: 1 Mar 2013
toBeautyString(28 Jan 2013)
expected answer 1 month and 1 days ago

Compare this with the previous calculation. We did both calculations on the 1st of March 2013. And in both cases we did calculate the same: toBeautyString(28 Jan 2013). But we are expecting two different results. Surprisingly both expectations are correct.

So, to deliver the result that really fits to our expectation, the algorithm should be able to read our mind. But this is not posible for an algorithm, so there can't be an algorithm that does perfectly what you are expecting.

Upvotes: -3

Kevin
Kevin

Reputation: 76234

It's helpful to recognize this as two distinct problems: 1) slicing the time into individual chunks of varying units; 2) formatting the chunks and joining them together with your choice of commas, conjunctions, etc. That way, you keep your text formatting logic separate from your time calculation logic.

#converts a time amount into a collection of time amounts of varying size.
#`increments` is a list that expresses the ratio of successive time units
#ex. If you want to split a time into days, hours, minutes, and seconds,
#increments should be [24,60,60]
#because there are 24 hours in a day, 60 minutes in an hour, etc.
#as an example, divideTime(100000, [24,60,60]) returns [1,3,46,40], 
#which is equivalent to 1 day, 3 hours, 46 minutes, 40 seconds
def divideTime(amount, increments):
    #base case: there's no increments, so no conversion is necessary
    if len(increments) == 0:
        return [amount]
    #in all other cases, we slice a bit off of `amount`,
    #give it to the smallest increment,
    #convert the rest of `amount` into the next largest unit, 
    #and solve the rest with a recursive call.
    else:
        conversionRate = increments[-1]
        smallestIncrement = amount % conversionRate
        rest = divideTime(amount / conversionRate, increments[:-1])
        return rest + [smallestIncrement]

def beautifulTime(amount):
    names      = ["year", "month", "day", "hour", "minute", "second"]
    increments = [12,     30,      24,    60,     60]
    ret = []
    times = divideTime(amount, increments)
    for i in range(len(names)):
        time = times[i]
        name = names[i]
        #don't display the unit if the time is zero
        #e.g. we prefer "1 year 1 second" to 
        #"1 year 0 months 0 days 0 hours 0 minutes 1 second"
        if time == 0:
            continue
        #pluralize name if appropriate
        if time != 1:
            name = name + "s"
        ret.append(str(time) + " " + name)
    #there's only one unit worth mentioning, so just return it
    if len(ret) == 1:
        return ret[0]
    #when there are two units, we don't need a comma
    if len(ret) == 2:
        return "{0} and {1}".format(ret[0], ret[1])
    #for all other cases, we want a comma and an "and" before the last unit
    ret[-1] = "and " + ret[-1]
    return ", ".join(ret)

print beautifulTime(100000000)
#output: 3 years, 2 months, 17 days, 9 hours, 46 minutes, and 40 seconds

This solution is somewhat inaccurate with regards to real-life years because it assumes a year is made up of 12 months, each 30 days long. This is a necessary abstraction, or otherwise you'd have to factor in varying month lengths and leap days and daylight savings time, etc etc etc. With this method, you'll lose about 3.75 days per year, which isn't so bad if you're only using it to visualize the magnitude of time spans.

Upvotes: 2

Related Questions