A Real Timespan Object With .Years & .Months
Asked Answered
C

8

29

Consider the following 2 scenarios: Scenario 1). Today is May 1st 2012, and Scenario 2). Today is September 1st 2012.

Now, consider that we write on our webpage the following about a comment someone has left: "This comment was written 3 months and 12 days ago". The amount of days in both these scenarios will ALWAYS be different even though the statement is exactly the same. In Scenario 1, "3 months and 12 days" would equal 102 days. However, in Scenario 2, "3 months and 12 days" would be 104 days!

Now, to corner in on my point, lets use a different example and say that someone left a comment on our site on Jan 30th 2013, and today is March 10th 2013. Our real TimeSpan object needs to know this relative date, and can figure out the following:

  • That there is 10 days in March,
  • That there is 1 day in Jan (counting from 30th to 31st).
  • That the month Feb is one month regardless of how many days there are in it (even though it's 28 days).

So, it would mean 10 days + 1 day + 1 month total, translating to This comment was posted 1 Month and 11 Days ago.

Now, if you used the MS style TimeSpan object (or any TimeSpan object in any language), it would give you the number of days from 30th Jan to 10 March (39 days), and because the TimeSpan object doesn't store relative date (the base/initial date we subtracted to get the TimeSpan), if you asked it how many months and days it has been, it will assume there is 30 days in one month, or even worst, the average which is greater than 30 days, and return the rest in days, so to get to 39 days, it will tell you it's been 1 Month and 9 Days and you will get the This comment was posted 1 Month and 9 Days ago message. Remember, both these scenarios have the same start date and same current/end date, yes the Microsoft TimeSpan object, by not allowing us to tell it the month of Feb 2013 should be considered, has given us a completely different TimeSpan, off by a whole 2 days. It has, in effect, lied to us.

The problem is, people will believe this, and who knows what perceptions they may have, how their perceptions of the past may change and the decisions & life choices they may make when trying to reconstruct events within the past inside their own minds, while never noticing or understanding the drawback and inherent failure of representing time that is so pervasive everywhere today. They will not understand that programming languages don't realize (or care) that last month had 31 days in it, as oppposed to 30, 29 or 28 - or visa versa, and that this adds up when you increase the TimeSpan.

This is the problem at the heart of this post. I understand that most people will not care about this difference (but be sure that some of us do, and cannot have this on our backs), and if this doesn't bother you, thats ok. I wish it didn't bother me, I would have saved myself some time, stress and disappointment. If this is not a bother, you can use the function for the efficient textual display of relative time (customizable to 1 to 6 nodes from seconds to years), instead of using it for the usually negligible accuracy it provides.

To my disappointment I noticed that there is no real timespan object, if you get a timespan, and do a .years or .months you'll get nothing, you'll only get .days and lower because a timeSpan object doesn't carry anything to tell it which month or year the timeSpan was created on. Therefore it'll never really know how many months it's been since days in each month vary over a year and even further over a leap year.

In response to this, I'll post a function I developed in order to get ACCURATE readings and be able to return things like the following on my ASP.NET web page...

Posted 4 years, 3 months, 14 days, 15 hours, 18 minutes and 24 seconds ago

I figured there'd be a …

timeSpan.GetActualNumberOf[Months/Days/Hours/etc] (base date must be provided of course)

… type method on this datatype, but there wasn't.

All you'd really have to do is create another property on the timeSpan object to give it a base date on which the difference was calculated, then the above lovely string would be calculable pretty easily, and a .year & .month would exist!

UPDATE: I have significantly expanded upon and updated my official answer and code usage details in my answer below, 100% working answer and code (in full), accurate and exact relative time/dates, no approximations - thanks.

Castellated answered 16/12, 2009 at 17:42 Comment(8)
kind of rant-y, but a reasonable question with some thought behind it and a code "solution" at leastDynatron
@Erx: Please do not add unnecessary text to your question, instead you could leave a comment here indicating that you've provided an answer below. Or better yet, Accept the answer below which you feels best answers your question.Gitlow
Closed as non-constructive as the definitions of month and year are fluid. If there was a fixed definition for both (or either, really), then it would be a constructive way of answering this question. Also, as an example, note that SO (developed with .NET) doesn't use any indicators beyond day to indicate the past, and I doubt that's because the TimeSpan structure doesn't have a representation on it.Astronavigation
@Astronavigation i can't believe you've closed a question thats several years old, Jeff Atwood had gotten one of the admins to re-open it when you or someone else closed it years ago, so i'm suggesting you re-open it since you're reasoning for closing it is actually not even close to being valid or reasonable. This is a factual/accuracy based question/answer, not philosophical or open to debate, arguments or polling. It actually provides and ads real value to the problem questioned. There is no point of removing value from stackoverflow, it is going against its purpose when you do this.Castellated
@Astronavigation I do not think this question should have been closed. I find your comment incorrect, there are fixed definitions for both months and years. Example: In astronomy, a Julian year (symbol: a) is a unit of measurement of time defined as exactly 365.25 days. Also note that January is defined as having exactly 31 days and similarly February is defined with fixed rules that can be programmed into a method or class. I am now looking for a real TimeSpan class and I am a bit unhappy that this question has been closed.Somewhat
@ben if you are looking for an accurate solution, please feel free to scroll down and read my Accepted Answer below. Brianary also provides a solution, but it will not solve the problem I have presented. Also, if you would like to reopen this question, please click on the "ReOpen" link underneath the question, once enough people click on it, the question will reopen and casperOne will not be able to re-close it again. :).Castellated
Good question, good answer, and very good complain about moderator trolling :-) I don't like when moderators close a question because they think I'm doing something wrong my app/sw/website... suggestion are always good, but moderators (@Astronavigation in this case) shouldn't stop me (or anyone) from asking question (and getting answer), just because he think I'm doing something wrong. In this question, the only "non-costructive" thing is stopping people from responding.Simarouba
Because business laws and contracts are often written in terms of years and months (and with specific guidance on calendar data calculations), it is a software requirement and has to be implemented according to the applicable rules. So, it is not as "fuzzy" as some have claimed. Instead, it may vary by jurisdiction. A discussion of variations of this calculation across different countries or legal systems may be useful.Marybethmaryellen
C
5

Here is the main answer with code, please note that you can get any number of dates/times accuracy, seconds & minutes, or seconds, minutes and days, anywhere up to years (which would contain 6 parts/segments). If you specify top two and it's over a year old, it will return "1 year and 3 months ago" and won't return the rest because you've requested two segments. if it's only a few hours old, then it will only return "2 hours and 1 minute ago". Of course, same rules apply if you specify 1, 2, 3, 4, 5 or 6 segmets (maxes out at 6 because seconds, minutes, hours, days, months, years only make 6 types). It will also correct grammer issues like "minutes" vs "minute" depending on if it's 1 minute or more, same for all types, and the "string" generated will always be grammatically correct.

Here are some examples for use: bAllowSegments identifies how many segments to show... ie: if 3, then return string would be (as an example)... "3 years, 2 months and 13 days" (won't include hours, minutes and seconds as the top 3 time categories are returned), if however, the date was a newer date, such as something a few days ago, specifying the same segments (3) will return "4 days, 1 hour and 13 minutes ago" instead, so it takes everything into account!

if bAllowSegments is 2 it would return "3 years and 2 months" and if 6 (maximum value) would return "3 years, 2 months, 13 days, 13 hours, 29 minutes and 9 seconds", but, be reminded that it will NEVER RETURN something like this "0 years, 0 months, 0 days, 3 hours, 2 minutes and 13 seconds ago" as it understands there is no date data in the top 3 segments and ignores them, even if you specify 6 segments, so don't worry :). Of course, if there is a segment with 0 in it, it will take that into account when forming the string, and will display as "3 days and 4 seconds ago" and ignoring the "0 hours" part! Enjoy and please comment if you like.

 Public Function RealTimeUntilNow(ByVal dt As DateTime, Optional ByVal bAllowSegments As Byte = 2) As String
  ' bAllowSegments identifies how many segments to show... ie: if 3, then return string would be (as an example)...
  ' "3 years, 2 months and 13 days" the top 3 time categories are returned, if bAllowSegments is 2 it would return
  ' "3 years and 2 months" and if 6 (maximum value) would return "3 years, 2 months, 13 days, 13 hours, 29 minutes and 9 seconds"
  Dim rYears, rMonths, rDays, rHours, rMinutes, rSeconds As Int16
  Dim dtNow = DateTime.Now
  Dim daysInBaseMonth = Date.DaysInMonth(dt.Year, dt.Month)

  rYears = dtNow.Year - dt.Year
  rMonths = dtNow.Month - dt.Month
  If rMonths < 0 Then rMonths += 12 : rYears -= 1 ' add 1 year to months, and remove 1 year from years.
  rDays = dtNow.Day - dt.Day
  If rDays < 0 Then rDays += daysInBaseMonth : rMonths -= 1
  rHours = dtNow.Hour - dt.Hour
  If rHours < 0 Then rHours += 24 : rDays -= 1
  rMinutes = dtNow.Minute - dt.Minute
  If rMinutes < 0 Then rMinutes += 60 : rHours -= 1
  rSeconds = dtNow.Second - dt.Second
  If rSeconds < 0 Then rSeconds += 60 : rMinutes -= 1

  ' this is the display functionality
  Dim sb As StringBuilder = New StringBuilder()
  Dim iSegmentsAdded As Int16 = 0

  If rYears > 0 Then sb.Append(rYears) : sb.Append(" year" & If(rYears <> 1, "s", "") & ", ") : iSegmentsAdded += 1
  If bAllowSegments = iSegmentsAdded Then GoTo parseAndReturn

  If rMonths > 0 Then sb.AppendFormat(rMonths) : sb.Append(" month" & If(rMonths <> 1, "s", "") & ", ") : iSegmentsAdded += 1
  If bAllowSegments = iSegmentsAdded Then GoTo parseAndReturn

  If rDays > 0 Then sb.Append(rDays) : sb.Append(" day" & If(rDays <> 1, "s", "") & ", ") : iSegmentsAdded += 1
  If bAllowSegments = iSegmentsAdded Then GoTo parseAndReturn

  If rHours > 0 Then sb.Append(rHours) : sb.Append(" hour" & If(rHours <> 1, "s", "") & ", ") : iSegmentsAdded += 1
  If bAllowSegments = iSegmentsAdded Then GoTo parseAndReturn

  If rMinutes > 0 Then sb.Append(rMinutes) : sb.Append(" minute" & If(rMinutes <> 1, "s", "") & ", ") : iSegmentsAdded += 1
  If bAllowSegments = iSegmentsAdded Then GoTo parseAndReturn

  If rSeconds > 0 Then sb.Append(rSeconds) : sb.Append(" second" & If(rSeconds <> 1, "s", "") & "") : iSegmentsAdded += 1

parseAndReturn:

  ' if the string is entirely empty, that means it was just posted so its less than a second ago, and an empty string getting passed will cause an error
  ' so we construct our own meaningful string which will still fit into the "Posted * ago " syntax...

  If sb.ToString = "" Then sb.Append("less than 1 second")

  Return ReplaceLast(sb.ToString.TrimEnd(" ", ",").ToString, ",", " and")

 End Function

Of course, you will need a "ReplaceLast" function, which takes a source string, and an argument specifying what needs to be replaced, and another arg specifying what you want to replace it with, and it only replaces the last occurance of that string... i've included my one if you don't have one or dont want to implement it, so here it is, it will work "as is" with no modification needed. I know the reverseit function is no longer needed (exists in .net) but the ReplaceLast and the ReverseIt func are carried over from the pre-.net days, so please excuse how dated it may look (still works 100% tho, been using em for over ten years, can guarante they are bug free)... :). Also, if you are using VB6, you can use StrReverse (wrapping it around the string extended with the .ReverseIt extension method), instead of using the ReverseIt() function (provided as an extension method). So, instead of doing sReplacable.ReverseIt, you'd do StrReverse(sReplacable) as StrReverse() is a built in VB6 function (and does the exact same thing, reverses a given string, and does nothing more). If you use StrReverse() instead of my generic ReverseIt function, feel free to delete the ReverseIt function/extension. StrReverse() function should be available in .NET as long as you are importing the legacy ms-visualbasic-dll library. Makes no difference either way, I had written ReverseIt() before I even know a StrReverse() function had existed, and had been using it ever since out of habit (no real reason to use mine as opposed to the in-built generic function StrReverse) - in fact, I'm sure StrReverse (or a similar, newer .NET specific version of a string reversing function) would be written to be more efficient :). cheers.

<Extension()> _ 
Public Function ReplaceLast(ByVal sReplacable As String, ByVal sReplaceWhat As String, ByVal sReplaceWith As String) As String 
    ' let empty string arguments run, incase we dont know if we are sending and empty string or not. 
    sReplacable = sReplacable.ReverseIt 
    sReplacable = Replace(sReplacable, sReplaceWhat.ReverseIt, sReplaceWith.ReverseIt, , 1) ' only does first item on reversed version! 
    Return sReplacable.ReverseIt.ToString 
End Function 

<Extension()> _ 
Public Function ReverseIt(ByVal strS As String, Optional ByVal n As Integer = -1) As String 
    Dim strTempX As String = "", intI As Integer 

    If n > strS.Length Or n = -1 Then n = strS.Length 

    For intI = n To 1 Step -1 
        strTempX = strTempX + Mid(strS, intI, 1) 
    Next intI 

    ReverseIt = strTempX + Right(strS, Len(strS) - n) 

End Function 
Castellated answered 24/12, 2009 at 1:32 Comment(3)
On Mar 1 of this year, RealTimeUntilNow(#2/28/2011#) will return 1 year, 1 day, which is wrong.Trochaic
@Trochaic what should the correct answer be? it depends on if you are counting 1 year based on number of days 2011 has (which is 365) or the number of days 2012 has (which is 366 days). Because 2010 has the extra feb 29th, it is using the latest year as definition of 1 year, and the 1st march is adding the extra day. I agree with you on the fact that choosing which year to use as the definition in this case is almost purely philosophical, if a particular year was encapsulated from start to end, only then is there no issue when it comes to defining that year, same goes for a particular month.Castellated
I think, if absolute precision it's what you are looking for, people expect 2/28 + 1 year = 2/28, regardless of year.Trochaic
T
29

Here's how to add some extension methods for this with C# using mean values:

public static class TimeSpanExtensions
{
    public static int GetYears(this TimeSpan timespan)
    {
        return (int)(timespan.Days/365.2425);
    }
    public static int GetMonths(this TimeSpan timespan)
    {
        return (int)(timespan.Days/30.436875);
    }
}
Trochaic answered 18/12, 2009 at 0:32 Comment(8)
I just wanted to add that this is an approximation, defeating the purpose of having the accurate timespan method discussed, although a good idea to save on CPU cycles, especialy on heavily trafficed websites. if you want 100% accurate method, please read my answer (and code) as I have updated it.Castellated
Actually this is a very accurate way to represent a timespan. What @Castellated wants isn't what the rest of the industry would call a timespan, but what ISO 8601 calls a time interval.Trochaic
first of all, i never suggested a different definition for the TimeSpan, all i wanted was the ability to pass a base date and get a true/accurate .Months & .Years on the timespan, are you understanding this? once again, no problem with definition, just shocked that a 100% accurate .Months & .Years was not built into the .NET framework, since almost every website is displaying relative time in the "Posted 3 months and 2 days ago" format. Its akin to .NET starting arrays from base 1 instead of base 0 because its easier to do, even tho it wastes one unit, and u know it is, dont say aintCastellated
and thanks for going around voting my posts down because you've suddenly developed a personal dislike for me. like i said, i like your approximation method, but it is definitely NOT a "very accurate" method, it is the best approximation you can do (so well done) that works for all years. But it does not solve the problem i've highlighted, so i need to point this out to people. You're still lying to people about relative dates, unless you leave the "days" parts out and just do months+years.Castellated
This diff just bugs me, so I must do the correction in order to sleep comfortably, and i know the fact that it bugs me bugs other coders, for some reason it bugs them to the core when I point this issue out (and it is an issue, even though the time difference is negligible, but potential for incorrect interpretations is what bothers me). would be interesting to understand why other coders hate me to the core when i point this out, psychoanalytically. is it because they think that i'm suggesting they are doing something incorrectly? (Because that wasn't my intention, I promise).Castellated
I haven't been "going around" down-voting you. And, yes, my method is accurate when you don't know the specific date. What you want, the problem you've highlighted, is a field-by-field subtraction of two datetimes, which is not a timespan. It's a difference, by definition. You cannot get that without using at least one of the datetimes. A timespan is an amount of time ("a time interval"), independent of a specific start or end datetime.Trochaic
This method fails with for example one year or one month that is 30 day or less long.Madge
Again, months and years aren't an exact measure, so converting a generic TimeSpan that doesn't have a particular starting point isn't going to be exact for all starting points, by definition. That this is one day off in only one case is the best anyone could really hope for under those conditions.Trochaic
S
12

What you are looking for is indeed not what TimeSpan represents. TimeSpan represents an interval as a count of ticks, without respect to a base DateTime or Calendar.

A new DateDifference type might make more sense here, with a constructor or factory method taking a base DateTime, a target DateTime, and optionally a Calendar (defaulting to CultureInfo.CurrentCulture) with which to compute the various difference components (years, months, etc.)

EDIT: It looks to me like Noda Time may have the tools you need for this — the Period class "[r]epresents a period of time expressed in human chronological terms: hours, days, weeks, months and so on", and in particular Period.Between(then, now, PeriodUnits.AllUnits) seems to be the precise calculation you're asking for — but it's necessarily a much more complex class than TimeSpan. The Key Concepts page on the Noda Time wiki explains how "humans make time messy":

Leaving aside the tricky bits of astronomy and relativity, mankind has still made time hard to negotiate. If we all used ticks from the Unix epoch to talk about time, there wouldn't be a need for a library like Noda Time.

But no, we like to talk in years, months, days, weeks - and for some reason we like 12pm (which confusingly comes before 1pm) to be roughly the time at which the sun is highest... so we have time zones.

Not only that, but we don't all agree on how many months there are. Different civilizations have come up with different ways of splitting up the year, and different numbers for the years to start with. These are calendar systems.

Shoemake answered 18/12, 2009 at 20:6 Comment(5)
hwo hard would it be to just provide a method of the timeSpan to get this information (without changing the data in timeSpan of course, so it still obviously returns its ticks)... such as .YearsSince(baseDate) and .MonthsSince(baseDate) ... in this context, the # of years and # of months will always be the same and only change as time changes to reflect yet another 100% accurate data... once again, this does not need to change the ticks, if it did of course .years & .months wouldn't work either.Castellated
The TimeSpan structure has existed since .NET 1.0, yet I don't recall ever hearing anyone else ask for this functionality. It would appear that most people don't have to deal with durations of over one month.Scratches
@John: that is probably because they are always using an approximation, all facebook and gmail posts always show time relative to a particular post or event, doing this without acknowledgement of the issue raised would certainly be an approximation, call me obsessive but i cannot live with providing inaccurate data to my users, which is why i was suprised, and needed to develop a solution which always gives the correct relative time based on the provided event/post date. relative time is used almost everywhere.Castellated
I think the issue is that the person posting the issue believes Microsoft should have added calendar-specific functionality (specifically, the modern Gregorian calendar) to a class, TimeSpan, that is fundamentally calendar-agnostic. I can understand his frustration at the seeming inability to get the years, or months between two points in time whose magnitude is a particular TimeSpan, but his proposed solution (that the methods be added to the TimeSpan itself) are, I think, misguided.Larimor
Edited: The issue is that Erx_VB.NExT believes msft should have added calendar-specific functionality (Gregorian calendar) to a class, TimeSpan, that is fundamentally calendar-agnostic. I can understand his frustration, but his proposed solution - adding methods to TimeSpan itself - is misguided. Having worked in Asia for over 8 years, I'm well aware of other calendars (in Thailand, it's year 2555 right now). Maybe his functions should be added to Globalization.Calendar, since the computation is calendar specific (Gregorian leap-years are not the same as for other calendars, for instance).Larimor
E
6

Well, better late then nothing I suppose ;)

C# function giving everything

And this is my modified version :

private string GetElapsedTime(DateTime from_date, DateTime to_date) {
int years;
int months;
int days;
int hours;
int minutes;
int seconds;
int milliseconds;

//------------------
// Handle the years.
//------------------
years = to_date.Year - from_date.Year;

//------------------------
// See if we went too far.
//------------------------
DateTime test_date = from_date.AddMonths(12 * years);

if (test_date > to_date)
{
    years--;
    test_date = from_date.AddMonths(12 * years);
}

//--------------------------------
// Add months until we go too far.
//--------------------------------
months = 0;

while (test_date <= to_date)
{
    months++;
    test_date = from_date.AddMonths(12 * years + months);
}

months--;

//------------------------------------------------------------------
// Subtract to see how many more days, hours, minutes, etc. we need.
//------------------------------------------------------------------
from_date = from_date.AddMonths(12 * years + months);
TimeSpan remainder = to_date - from_date;
days = remainder.Days;
hours = remainder.Hours;
minutes = remainder.Minutes;
seconds = remainder.Seconds;
milliseconds = remainder.Milliseconds;

return (years > 0 ? years.ToString() + " years " : "") +
       (months > 0 ? months.ToString() + " months " : "") +
       (days > 0 ? days.ToString() + " days " : "") +
       (hours > 0 ? hours.ToString() + " hours " : "") +
       (minutes > 0 ? minutes.ToString() + " minutes " : "");}
Emee answered 16/12, 2009 at 17:42 Comment(0)
C
5

Here is the main answer with code, please note that you can get any number of dates/times accuracy, seconds & minutes, or seconds, minutes and days, anywhere up to years (which would contain 6 parts/segments). If you specify top two and it's over a year old, it will return "1 year and 3 months ago" and won't return the rest because you've requested two segments. if it's only a few hours old, then it will only return "2 hours and 1 minute ago". Of course, same rules apply if you specify 1, 2, 3, 4, 5 or 6 segmets (maxes out at 6 because seconds, minutes, hours, days, months, years only make 6 types). It will also correct grammer issues like "minutes" vs "minute" depending on if it's 1 minute or more, same for all types, and the "string" generated will always be grammatically correct.

Here are some examples for use: bAllowSegments identifies how many segments to show... ie: if 3, then return string would be (as an example)... "3 years, 2 months and 13 days" (won't include hours, minutes and seconds as the top 3 time categories are returned), if however, the date was a newer date, such as something a few days ago, specifying the same segments (3) will return "4 days, 1 hour and 13 minutes ago" instead, so it takes everything into account!

if bAllowSegments is 2 it would return "3 years and 2 months" and if 6 (maximum value) would return "3 years, 2 months, 13 days, 13 hours, 29 minutes and 9 seconds", but, be reminded that it will NEVER RETURN something like this "0 years, 0 months, 0 days, 3 hours, 2 minutes and 13 seconds ago" as it understands there is no date data in the top 3 segments and ignores them, even if you specify 6 segments, so don't worry :). Of course, if there is a segment with 0 in it, it will take that into account when forming the string, and will display as "3 days and 4 seconds ago" and ignoring the "0 hours" part! Enjoy and please comment if you like.

 Public Function RealTimeUntilNow(ByVal dt As DateTime, Optional ByVal bAllowSegments As Byte = 2) As String
  ' bAllowSegments identifies how many segments to show... ie: if 3, then return string would be (as an example)...
  ' "3 years, 2 months and 13 days" the top 3 time categories are returned, if bAllowSegments is 2 it would return
  ' "3 years and 2 months" and if 6 (maximum value) would return "3 years, 2 months, 13 days, 13 hours, 29 minutes and 9 seconds"
  Dim rYears, rMonths, rDays, rHours, rMinutes, rSeconds As Int16
  Dim dtNow = DateTime.Now
  Dim daysInBaseMonth = Date.DaysInMonth(dt.Year, dt.Month)

  rYears = dtNow.Year - dt.Year
  rMonths = dtNow.Month - dt.Month
  If rMonths < 0 Then rMonths += 12 : rYears -= 1 ' add 1 year to months, and remove 1 year from years.
  rDays = dtNow.Day - dt.Day
  If rDays < 0 Then rDays += daysInBaseMonth : rMonths -= 1
  rHours = dtNow.Hour - dt.Hour
  If rHours < 0 Then rHours += 24 : rDays -= 1
  rMinutes = dtNow.Minute - dt.Minute
  If rMinutes < 0 Then rMinutes += 60 : rHours -= 1
  rSeconds = dtNow.Second - dt.Second
  If rSeconds < 0 Then rSeconds += 60 : rMinutes -= 1

  ' this is the display functionality
  Dim sb As StringBuilder = New StringBuilder()
  Dim iSegmentsAdded As Int16 = 0

  If rYears > 0 Then sb.Append(rYears) : sb.Append(" year" & If(rYears <> 1, "s", "") & ", ") : iSegmentsAdded += 1
  If bAllowSegments = iSegmentsAdded Then GoTo parseAndReturn

  If rMonths > 0 Then sb.AppendFormat(rMonths) : sb.Append(" month" & If(rMonths <> 1, "s", "") & ", ") : iSegmentsAdded += 1
  If bAllowSegments = iSegmentsAdded Then GoTo parseAndReturn

  If rDays > 0 Then sb.Append(rDays) : sb.Append(" day" & If(rDays <> 1, "s", "") & ", ") : iSegmentsAdded += 1
  If bAllowSegments = iSegmentsAdded Then GoTo parseAndReturn

  If rHours > 0 Then sb.Append(rHours) : sb.Append(" hour" & If(rHours <> 1, "s", "") & ", ") : iSegmentsAdded += 1
  If bAllowSegments = iSegmentsAdded Then GoTo parseAndReturn

  If rMinutes > 0 Then sb.Append(rMinutes) : sb.Append(" minute" & If(rMinutes <> 1, "s", "") & ", ") : iSegmentsAdded += 1
  If bAllowSegments = iSegmentsAdded Then GoTo parseAndReturn

  If rSeconds > 0 Then sb.Append(rSeconds) : sb.Append(" second" & If(rSeconds <> 1, "s", "") & "") : iSegmentsAdded += 1

parseAndReturn:

  ' if the string is entirely empty, that means it was just posted so its less than a second ago, and an empty string getting passed will cause an error
  ' so we construct our own meaningful string which will still fit into the "Posted * ago " syntax...

  If sb.ToString = "" Then sb.Append("less than 1 second")

  Return ReplaceLast(sb.ToString.TrimEnd(" ", ",").ToString, ",", " and")

 End Function

Of course, you will need a "ReplaceLast" function, which takes a source string, and an argument specifying what needs to be replaced, and another arg specifying what you want to replace it with, and it only replaces the last occurance of that string... i've included my one if you don't have one or dont want to implement it, so here it is, it will work "as is" with no modification needed. I know the reverseit function is no longer needed (exists in .net) but the ReplaceLast and the ReverseIt func are carried over from the pre-.net days, so please excuse how dated it may look (still works 100% tho, been using em for over ten years, can guarante they are bug free)... :). Also, if you are using VB6, you can use StrReverse (wrapping it around the string extended with the .ReverseIt extension method), instead of using the ReverseIt() function (provided as an extension method). So, instead of doing sReplacable.ReverseIt, you'd do StrReverse(sReplacable) as StrReverse() is a built in VB6 function (and does the exact same thing, reverses a given string, and does nothing more). If you use StrReverse() instead of my generic ReverseIt function, feel free to delete the ReverseIt function/extension. StrReverse() function should be available in .NET as long as you are importing the legacy ms-visualbasic-dll library. Makes no difference either way, I had written ReverseIt() before I even know a StrReverse() function had existed, and had been using it ever since out of habit (no real reason to use mine as opposed to the in-built generic function StrReverse) - in fact, I'm sure StrReverse (or a similar, newer .NET specific version of a string reversing function) would be written to be more efficient :). cheers.

<Extension()> _ 
Public Function ReplaceLast(ByVal sReplacable As String, ByVal sReplaceWhat As String, ByVal sReplaceWith As String) As String 
    ' let empty string arguments run, incase we dont know if we are sending and empty string or not. 
    sReplacable = sReplacable.ReverseIt 
    sReplacable = Replace(sReplacable, sReplaceWhat.ReverseIt, sReplaceWith.ReverseIt, , 1) ' only does first item on reversed version! 
    Return sReplacable.ReverseIt.ToString 
End Function 

<Extension()> _ 
Public Function ReverseIt(ByVal strS As String, Optional ByVal n As Integer = -1) As String 
    Dim strTempX As String = "", intI As Integer 

    If n > strS.Length Or n = -1 Then n = strS.Length 

    For intI = n To 1 Step -1 
        strTempX = strTempX + Mid(strS, intI, 1) 
    Next intI 

    ReverseIt = strTempX + Right(strS, Len(strS) - n) 

End Function 
Castellated answered 24/12, 2009 at 1:32 Comment(3)
On Mar 1 of this year, RealTimeUntilNow(#2/28/2011#) will return 1 year, 1 day, which is wrong.Trochaic
@Trochaic what should the correct answer be? it depends on if you are counting 1 year based on number of days 2011 has (which is 365) or the number of days 2012 has (which is 366 days). Because 2010 has the extra feb 29th, it is using the latest year as definition of 1 year, and the 1st march is adding the extra day. I agree with you on the fact that choosing which year to use as the definition in this case is almost purely philosophical, if a particular year was encapsulated from start to end, only then is there no issue when it comes to defining that year, same goes for a particular month.Castellated
I think, if absolute precision it's what you are looking for, people expect 2/28 + 1 year = 2/28, regardless of year.Trochaic
B
2

Using .Net 4.5 and the CultureInfo class, one can add months and years to a given date.

DateTime datetime = DateTime.UtcNow;
int years = 15;
int months = 7;

DateTime yearsAgo = CultureInfo.InvariantCulture.Calendar.AddYears(datetime, -years);
DateTime monthsInFuture = CultureInfo.InvariantCulture.Calendar.AddMonths(datetime, months);

Since that's a lot of typing, I prefer to create extension methods:

public static DateTime AddYears(this DateTime datetime, int years)
{
    return CultureInfo.InvariantCulture.Calendar.AddYears(datetime, years);
}

public static DateTime AddMonths(this DateTime datetime, int months)
{
    return CultureInfo.InvariantCulture.Calendar.AddMonths(datetime, months);
}

DateTime yearsAgo = datetime.AddYears(-years);
DateTime monthsInFuture = datetime.AddMonths(months);
Bataan answered 16/12, 2009 at 17:42 Comment(0)
L
1

I would say that the current TimeSpan is a real timespan object, i.e., the amount of time between Jan 1 2008 1:31 a.m. and Feb. 3, 2008 at 6:45 a.m. is the same as the amount of time between Feb. 5, 2008 at 1:45 p.m. and March 9, 2008 at 6:59 p.m.. What you are looking for is in actuality the difference between two datetimes.

As for the .MakeMagicHappen.gimmeSomethingPretty.surelyMShasThoughtAboutThisDilema to fulfill the specific needs of your system, that's why people hire you as a programmer. If the framework you are using does absolutely everything, your company would just be able to presss a single button and their system would pop out fully formed and you'd be on the unemployment line along with the rest of us programmers.

Leaf answered 16/12, 2009 at 18:32 Comment(21)
no it is not a real timeSpan in that it doesn't contain something which it should, yes it gives accurate data, but when you want the data in terms of years and months relative to a base time, it is not provided. there are many sites that display time time in this format, "b years, c months and d days ago", what most people do is just use an approximation in terms of years/months, this is vastly unsettling for me so had to find an accurate solution. it's used often @ gmail, facebook etc - all display time after a specific event, of course it should have been included in the framework!Castellated
i say this because, including this in a timeSpan does not remove any accuracy, of course, the information is still 100% accurate since it's relative to a base time/date, many sites use it, and they all probably use an approximation because ms does not have it included, i find it hard to absorb why you would disagree with that, anyway, maybe implementing empty classes are your thing, fair enough, check out .net 1.0, i hear that has a lot of unfinished classesCastellated
Your first paragraph can be shown to be false with varying time zones.Necrosis
just make sure you send server timezone as dt, the timediff will still be the same :)Castellated
@Jeff: ps: im not looking for the time difference between two objects, im looking for a worded expression to display to users, in terms of nyears, b months and d days ago....WITHOUT compromising the accuracy of the time displayed - this was the problem to begin with... i apologize as i don't think i described the problem clearly in my message, however i hope this clarifies it for you and those who may be confused. thx -erxCastellated
@jeff: also to be clear, just a .months (since base time) and .years (since base time) would have really been all thats required from the vs framework... that is not a specific need for my application (the strings format i need clearly would be, but they do have a .ToLongTimeString and .ToLongDateString on the datetime object)...Castellated
@Erx_VB.NExT.Coder: How many months and days are represented by TimeSpan.FromDays(60)?Respite
@Erx_VB.NExT.Coder: You're having a semantics argument. What the industry calls a timespan isn't what you call a timespan.Trochaic
@Trochaic that comment i made you are refering to is NOT the thesis of the discussion, in fact, I can even remove it if need be. The point and only point of the post is to resolve the difference in time that occurs using common/typical date/time estimation methods, this is all. Once again, the comment you're refering to is a side statement and not the point, purpose or reason behind the post (and this should be obvious). Once again, the comment is just an expressed opinion, and its NOT my actual question! I should be allowed to ask a question & express a non-question side opinionative comment.Castellated
@Respite re .FromDays(60) it depends what the base date was, if it was 1st Feb, 2012 vs 1st Feb 2013, the outcome will be different. If it was even any two different months in the same year, it would most likely still be different. say 1st april vs 1st may, the number of months will be the same but number of days will be different, even tho the 60 days are fixed in both cases. anyway, i have updated the post again and included better explanation and examples.Castellated
@Erx_VB.NExT.Coder: Having a method which takes two DateTimes and describes the difference between them may sometimes be useful, but there are enough variations in the business rules associated with such things, even with the "InvariantCulture" calendar, that making it a Framework class doesn't seem warranted. For example, what date would be regarded as precisely one month before March 31, 2012? What about March 30, 2012?Respite
@Respite I think i sucked at explaining this, hence people disagreeing. But have updated main post, will try again. So 31 days would be considered as one full month if the month is march 2012. If we are referring to the full month of April 2012, it will be 30 days and will be considered as 1 month only. So, since today is Feb 14th 2012, 45 days ago would mean 1 Month and 14 days ago because last month contains 31 days. If last month contained 30 days, then the same amount of time (45 days) would be written as "1 Month and 15 days" ago. I hope this was a better explanation. Also...Castellated
@Respite I think I know what you're getting at. If today is 20th March, then how many days are in the wording "1 Month Ago", correct? because then there is the issue of: do we use Feb's 29 Days in a month definition of "1 Month" or do we use March's 31 days in a month definition. I will not advocate one way or the other, however, I would use last months definition since the month had completed, and when you say "1 Month Ago" you can't use March's definition as it hasn't been reached yet. I get your point and it is a good point that you raise, a very good point indeed. Also...Castellated
@Respite If one wants, they can use the definition based on which month contains the most days in "1 Month Ago", or split get the average days of the two months in question in a 50/50 share, or even doing a biased average instead, where if 2/3rds of the days are in March, then the days in March are given 2/3rds weighting and days in Feb getting 1/3rds weighting giving the biased average (((29 * 1/3) + (31 * 2/3)) / 2). However, I am listing these options because I do not want to advocate any, however, my personal opinion, I wouldnt use any of these methods, I would use ... (continued)...Castellated
@Respite ... (continued)... I would use the first one I mentioned, meaning, the number of days in the last completed month would have the definition of "1 Month", as it wouldn't make perfect sense (to me) to use the current months definition as it hasn't been reached or completed, so when you tell someone "1 Month Ago" it is refering to the past, and the last completed month. However, you can't ask whats 1 month before march 31st or 30th, since you need a base date and therefore need to know the number of days. (cont)...Castellated
@Respite ...(cont) If 30 days ago, then it wouldn't be a month since it doesn't reach Feb, and would be "30 days ago", 1st March. If you ask, same question and today is 30th March, then it would be 29th Feb, and therefore "1 Month and 1 day ago" because it touches the previous month. If, however, you wanted to use the current month, or month with most days in it, then 1 Month before 31st March would be 29 Feb (last day of Feb, since its the last day of March) and for 30th March would be second last day of Feb (28th) since it is the 2nd last day of March.Castellated
@Erx_VB.NExT.Coder: Things like monthly rental contracts can have varying rules for how these issues should be handled. Even yearly contracts can have odd cases (e.g. if someone rents something for a year on Feb 29, and returns it Mar 2 of the following year, how late is it?)Respite
@Respite you're right, these are issues & things that would need to be outlined in their contract terms (but i highly doubt are since I remember reading one entirely with no mention of it, lol). Also, I was thinking about this last night, & if MS wanted to incorporate something like this, they could give us options on how to deal with the various situations (those described above) or they could just ensure if a whole month isn't fully encapsulated from start to end, then state it in days, so you might have things like "60 Days" ago... (cont)...Castellated
@Respite (cnt) but anything above that will be able to have months without issues. Though, if you asked me, I totally would NOT want to be putting things like "3 Months & 60 Days ago" anywhere on my websites lol... so the best way to deal with that still remains. Maybe doing a landlords approximation may be the best way to deal with two orphaned months (i.e.: when a TimeSpan spans two different months without fully encapsulating them from start to finish). I wonder if this question of how to deal with Orphaned Months would be a good idea for a community wiki, if anyone thinks so, I will post.Castellated
@Erx_VB.NExT.Coder: My personal recommendation would probably be to use fractional months; count the tail fraction of the starting month, plus some number of whole months, plus the leading fraction of the ending month. So from noon on Feb 2, 2012, to noon on March 30 would be (27.5/29 + 29.5/31.) is about 1.9 months.Respite
This really should have been a comment on the question.Meatus
A
1

I believe that the following method is pretty trusteable and straightforward, since it's based on the framework date calculation and returns a readable elapsed time strings like Facebook's ones. Sorry about the little portuguese words and plural treatment, in my case it was necessary.

public static string ElapsedTime(DateTime dtEvent)
{
    TimeSpan TS = DateTime.Now - dtEvent;

    int intYears = TS.Days / 365;
    int intMonths = TS.Days / 30;
    int intDays = TS.Days;
    int intHours = TS.Hours;
    int intMinutes = TS.Minutes;
    int intSeconds = TS.Seconds;

    if (intYears > 0) return String.Format("há {0} {1}", intYears, (intYears == 1) ? "ano" : "anos");
    else if (intMonths > 0) return String.Format("há {0} {1}", intMonths, (intMonths == 1) ? "mês" : "meses");
    else if (intDays > 0) return String.Format("há {0} {1}", intDays, (intDays == 1) ? "dia" : "dias");
    else if (intHours > 0) return String.Format("há ± {0} {1}", intHours, (intHours == 1) ? "hora" : "horas");
    else if (intMinutes > 0) return String.Format("há ± {0} {1}", intMinutes, (intMinutes == 1) ? "minuto" : "minutos");
    else if (intSeconds > 0) return String.Format("há ± {0} {1}", intSeconds, (intSeconds == 1) ? "segundo" : "segundos");
    else
    {
        return String.Format("em {0} às {1}", dtEvent.ToShortDateString(), dtEvent.ToShortTimeString());
    }
}
Adjure answered 23/5, 2011 at 19:1 Comment(2)
thanks for your contribution, but your calculation may yield a result a few days off due to not accounting for varying days in each month. But up voted anyway :)Castellated
This is excactly how to reproduce the bug @Castellated ranted about in the op. (yes, I consider it a bug). About the portuguese words: pode postar na boa.Bonnice
B
0

I took the accepted answer and converted it from VB.Net to C# and made a few modification/improvements as well. I got rid of the string reversals, which were being used to replace the last instance of a string, and used an extension method that more directly finds and replaces the last instance of a string.

Example of how to call the method:

PeriodBetween(#2/28/2011#, DateTime.UtcNow, 6)

Main Method:

public static string PeriodBetween(DateTime then, DateTime now, byte numberOfPeriodUnits = 2)
{
    // Translated from VB.Net to C# from: https://stackoverflow.com/a/1956265

    // numberOfPeriodUnits identifies how many time period units to show.
    // If numberOfPeriodUnits = 3, function would return:
    //      "3 years, 2 months and 13 days"
    // If numberOfPeriodUnits = 2, function would return:
    //      "3 years and 2 months"
    // If numberOfPeriodUnits = 6, (maximum value), function would return:
    //      "3 years, 2 months, 13 days, 13 hours, 29 minutes and 9 seconds"

    if (numberOfPeriodUnits > 6 || numberOfPeriodUnits < 1)
    {
        throw new ArgumentOutOfRangeException($"Parameter [{nameof(numberOfPeriodUnits)}] is out of bounds. Valid range is 1 to 6.");
    }

    short Years = 0;
    short Months = 0;
    short Days = 0;
    short Hours = 0;
    short Minutes = 0;
    short Seconds = 0;
    short DaysInBaseMonth = (short)(DateTime.DaysInMonth(then.Year, then.Month));

    Years = (short)(now.Year - then.Year);

    Months = (short)(now.Month - then.Month);
    if (Months < 0)
    {
        Months += 12;
        Years--; // add 1 year to months, and remove 1 year from years.
    }

    Days = (short)(now.Day - then.Day);
    if (Days < 0)
    {
        Days += DaysInBaseMonth;
        Months--;
    }

    Hours = (short)(now.Hour - then.Hour);
    if (Hours < 0)
    {
        Hours += 24;
        Days--;
    }

    Minutes = (short)(now.Minute - then.Minute);
    if (Minutes < 0)
    {
        Minutes += 60;
        Hours--;
    }

    Seconds = (short)(now.Second - then.Second);
    if (Seconds < 0)
    {
        Seconds += 60;
        Minutes--;
    }

    // This is the display functionality.
    StringBuilder TimePeriod = new StringBuilder();
    short NumberOfPeriodUnitsAdded = 0;

    if (Years > 0)
    {
        TimePeriod.Append(Years);
        TimePeriod.Append(" year" + (Years != 1 ? "s" : "") + ", ");
        NumberOfPeriodUnitsAdded++;
    }

    if (numberOfPeriodUnits == NumberOfPeriodUnitsAdded)
    {
        goto ParseAndReturn;
    }

    if (Months > 0)
    {
        TimePeriod.AppendFormat(Months.ToString());
        TimePeriod.Append(" month" + (Months != 1 ? "s" : "") + ", ");
        NumberOfPeriodUnitsAdded++;
    }

    if (numberOfPeriodUnits == NumberOfPeriodUnitsAdded)
    {
        goto ParseAndReturn;
    }

    if (Days > 0)
    {
        TimePeriod.Append(Days);
        TimePeriod.Append(" day" + (Days != 1 ? "s" : "") + ", ");
        NumberOfPeriodUnitsAdded++;
    }

    if (numberOfPeriodUnits == NumberOfPeriodUnitsAdded)
    {
        goto ParseAndReturn;
    }

    if (Hours > 0)
    {
        TimePeriod.Append(Hours);
        TimePeriod.Append(" hour" + (Hours != 1 ? "s" : "") + ", ");
        NumberOfPeriodUnitsAdded++;
    }

    if (numberOfPeriodUnits == NumberOfPeriodUnitsAdded)
    {
        goto ParseAndReturn;
    }

    if (Minutes > 0)
    {
        TimePeriod.Append(Minutes);
        TimePeriod.Append(" minute" + (Minutes != 1 ? "s" : "") + ", ");
        NumberOfPeriodUnitsAdded++;
    }

    if (numberOfPeriodUnits == NumberOfPeriodUnitsAdded)
    {
        goto ParseAndReturn;
    }

    if (Seconds > 0)
    {
        TimePeriod.Append(Seconds);
        TimePeriod.Append(" second" + (Seconds != 1 ? "s" : "") + "");
        NumberOfPeriodUnitsAdded++;
    }

    ParseAndReturn:
    // If the string is empty, that means the datetime is less than a second in the past.
    // An empty string being passed will cause an error, so we construct our own meaningful
    // string which will still fit into the "Posted * ago " syntax.

    if (TimePeriod.ToString() == "")
    {
        TimePeriod.Append("less than 1 second");
    }

    return TimePeriod.ToString().TrimEnd(' ', ',').ToString().ReplaceLast(",", " and");
}

ReplaceLast Extension Method:

public static string ReplaceLast(this string source, string search, string replace)
{
    int pos = source.LastIndexOf(search);

    if (pos == -1)
    {
        return source;
    }

    return source.Remove(pos, search.Length).Insert(pos, replace);
}
Banking answered 16/12, 2009 at 17:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.