+1  A: 

had to create own .reverse method as the ms - .reverse.tostring doesn't return what i would have liked it to... and excuse the use of the mid() function in the reverseIt dependancy, as it was carried over from vb6 days :p

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




<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
Erx_VB.NExT.Coder
returns string while making sure the word AND is included on the last seconds or minutes or whichever time element ends up coming last... instead of a comma, so end in word and... eg: full return sample4 years, 3 months, 14 days, 15 hours, 18 minutes and 24 seconds
Erx_VB.NExT.Coder
What about differences in time zones?
Austin Salonen
hey, that wasn't needed, you would of course make sure the time your sending is the same time zone, and you can change the code around if you want... timezone logic is usually handled elsewhere
Erx_VB.NExT.Coder
Ewww... VB.NET - stay away from me.
Baddie
Erx_VB.NExT.Coder
+3  A: 

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.

Jeff Hornby
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!
Erx_VB.NExT.Coder
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 classes
Erx_VB.NExT.Coder
Your first paragraph can be shown to be false with varying time zones.
Austin Salonen
just make sure you send server timezone as dt, the timediff will still be the same :)
Erx_VB.NExT.Coder
@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 -erx
Erx_VB.NExT.Coder
@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)...
Erx_VB.NExT.Coder
+3  A: 

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)((double)timespan.Days/365.2425);
 }
 public static int GetMonths(this TimeSpan timespan)
 {
  return (int)((double)timespan.Days/30.436875);
 }
}
brianary
thanks brian, that's a fair approximation which im sure some many would prefer
Erx_VB.NExT.Coder
+1  A: 

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.)

Jeffrey Hantin
Erx_VB.NExT.Coder
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.
John Saunders
@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.
Erx_VB.NExT.Coder
+2  A: 

updated main part of the code...

 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
Erx_VB.NExT.Coder