Results 1 to 15 of 15

Thread: Elegant code for TimeSince()

  1. #1

    Thread Starter
    PowerPoster Ellis Dee's Avatar
    Join Date
    Mar 2007
    Location
    New England
    Posts
    3,530

    Elegant code for TimeSince()

    I'm writing what I thought was going to be a simple function to describe a time in the past, but it's quickly becoming way more complex than I expected.

    The purpose is for a personal backup routine, so I don't technically "need" this, but it's become a puzzle I'm fascinated by. I'm hoping there's some clever tricks with DateSerial() or whatever to tighten this code up from being a total mess.

    The goal is to be able to go back an arbitrary amount of time, up to years (but not decades) and always return a string with the two most significant values. Sample outputs would be:

    37 seconds ago
    13 minutes, 5 seconds ago
    1 hour, 12 minutes ago
    3 days, 4 hours ago
    2 weeks, 1 day ago
    6 months, 2 weeks ago
    1 year, 2 months ago

    I start by DateDiff()ing the difference in seconds. A long can go up to 68 years worth of seconds, so that's plenty. I also set up a helper function to handle pluralization because that's how I roll:

    Code:
    Private Function Plural(plngNumber As Long, pstrSingular As String) As String
        If plngNumber = 1 Then Plural = "1 " & pstrSingular Else Plural = plngNumber & " " & pstrSingular & "s"
    End Function
    Things started getting weird when I got to "x months, y weeks." Here's what I have so far:

    vb Code:
    1. ' Arbitrary value for "blank" date
    2. Public Const NullDate As Date = #1/27/1991# ' WIDE RIGHT!!!!!
    3.  
    4. Public Function TimeSince(pdtmDate As Date) As String
    5.     Dim lngYears As Long
    6.     Dim lngMonths As Long
    7.     Dim lngWeeks As Long
    8.     Dim lngDays As Long
    9.     Dim lngHours As Long
    10.     Dim lngMinutes As Long
    11.     Dim lngSeconds As Long
    12.     Dim strReturn As String
    13.    
    14.     Do While pdtmDate <> NullDate
    15.         ' Seconds
    16.         lngSeconds = DateDiff("s", pdtmDate, Now())
    17.         If lngSeconds <= 60 Then
    18.             strReturn = Plural(lngSeconds, "second")
    19.             Exit Do
    20.         End If
    21.         ' Minutes, Seconds
    22.         lngMinutes = lngSeconds \ 60
    23.         lngSeconds = lngSeconds - lngMinutes * 60
    24.         If lngMinutes <= 60 Then
    25.             strReturn = Plural(lngMinutes, "minute") & ", " & Plural(lngSeconds, "second")
    26.             Exit Do
    27.         End If
    28.         ' Hours, Minutes
    29.         lngHours = lngMinutes \ 60
    30.         lngMinutes = lngMinutes - lngHours * 60
    31.         If lngHours <= 24 Then
    32.             strReturn = Plural(lngHours, "hour") & ", " & Plural(lngMinutes, "minute")
    33.             Exit Do
    34.         End If
    35.         ' Days, Hours
    36.         lngDays = lngHours \ 24
    37.         lngHours = lngHours - lngDays * 24
    38.         If lngDays <= 7 Then
    39.             strReturn = Plural(lngDays, "day") & ", " & Plural(lngHours, "hour")
    40.             Exit Do
    41.         End If
    42.         ' Weeks, Days
    43.         If pdtmDate >= DateAdd("m", -1, Now()) Then
    44.             lngWeeks = lngDays \ 7
    45.             lngDays = lngDays - lngWeeks * 7
    46.             strReturn = Plural(lngWeeks, "week") & ", " & Plural(lngDays, "day")
    47.             Exit Do
    48.         End If
    49.         ' Months, Weeks
    50.         ' ???
    51.         ' Years, Months
    52.         ' ???
    53.         Exit Do
    54.     Loop
    55.     If Len(strReturn) Then strReturn = strReturn & " ago" Else strReturn = "Never"
    56.     TimeSince = strReturn
    57. End Function
    Last edited by Ellis Dee; Mar 7th, 2016 at 05:28 PM.

  2. #2
    PowerPoster Arnoutdv's Avatar
    Join Date
    Oct 2013
    Posts
    6,747

    Re: Elegant code for TimeSince()

    I thought I had an "elegant" solution, but it's not correct
    The problem being DateDiff() returning integers and literally doing (A - B) + 1, for example for years.
    Then I also read this in the help for DateDiff

    When comparing December 31 to January 1 of the immediately succeeding year, DateDiff for Year ("yyyy") returns 1 even though only a day has elapsed.
    Not correct working code ahead:
    Code:
    Private Function TimeSince(dtDate As Date) As String
      Dim dtNow As Date
      Dim lValues(6) As Long ' year, month, week, day, hour, minute, second
      Dim lDiff As Long
      Dim aText() As String
      Dim i As Long
      
      If dtDate = 0 Then Exit Function
      
      aText = Split("years,months,weeks,days,hours,minutes,seconds", ",")
      dtNow = Now
      
      lValues(0) = DateDiff("yyyy", dtDate, dtNow)        ' number of year
      lValues(1) = DateDiff("m", dtDate, dtNow) Mod 12    ' number of months
      lDiff = DateDiff("d", dtDate, dtNow)
      lValues(2) = (lDiff / 7) Mod 5                      ' number of weeks
      lValues(3) = lDiff Mod 7                            ' number of days
      lValues(4) = DateDiff("h", dtDate, dtNow) Mod 24    ' number of hours
      lValues(5) = DateDiff("n", dtDate, dtNow) Mod 60    ' number of minutes
      lValues(6) = DateDiff("s", dtDate, dtNow) Mod 60    ' number of seconds
      
      For i = 0 To 6
        If lValues(i) <> 0 Then
          TimeSince = lValues(i) & " " & aText(i)
          If i <> 6 Then
            TimeSince = TimeSince & ", " & lValues(i + 1) & " " & aText(i + 1)
          End If
          Exit For
        End If
      Next i
    End Function

  3. #3
    Super Moderator si_the_geek's Avatar
    Join Date
    Jul 2002
    Location
    Bristol, UK
    Posts
    41,974

    Re: Elegant code for TimeSince()

    I've written similar code to post #1 before, and I don't think I've seen anything noticeably better... but the months and years are a bit tricky, as you need to take into account varying days per month and leap years.

    I can't find the posts here I've seen over the years that solved it nicely... however, you can do this to calculate the months:
    Code:
            lngMonths = (Year(Now) - Year(pdtmDate)) * 12 + (Month(Now) - Month(pdtmDate))
            If Day(Now) < Day(pdtmDate) Then lngMonths = lngMonths -1
    ...but in order to get the weeks too, you need to recalculate ignoring the months:
    Code:
            lngWeeks = DateDiff("d", DateAdd("m", pdtmDate, lngMonths), Now) \ 7
    Once you have the months, the "Years, Months" calculation is along the lines of:
    Code:
            lngYears = lngMonths \ 12
            lngMonths = lngMonths Mod 12

    All this code is written directly to the forums so may have minor mistakes, but should be close enough to work it out.

  4. #4

    Thread Starter
    PowerPoster Ellis Dee's Avatar
    Join Date
    Mar 2007
    Location
    New England
    Posts
    3,530

    Re: Elegant code for TimeSince()

    Quote Originally Posted by si_the_geek View Post
    I
    Code:
            lngMonths = (Year(Now) - Year(pdtmDate)) * 12 + (Month(Now) - Month(pdtmDate))
            If Day(Now) < Day(pdtmDate) Then lngMonths = lngMonths -1
    ...but in order to get the weeks too, you need to recalculate ignoring the months:
    Code:
            lngWeeks = DateDiff("d", DateAdd("m", pdtmDate, lngMonths), Now) \ 7
    That makes sense, thanks.

    Once you have the months, the "Years, Months" calculation is along the lines of:
    Code:
            lngYears = lngMonths \ 12
            lngMonths = lngMonths Mod 12
    My function is obviously not very efficient, but I've avoided the Mod operator for years just on principle after learning on these forums that it executes (comparatively) slowly. Do you know if Mod is noticeable slower than my multiplication alternative?

  5. #5

    Thread Starter
    PowerPoster Ellis Dee's Avatar
    Join Date
    Mar 2007
    Location
    New England
    Posts
    3,530

    Re: Elegant code for TimeSince()

    After a good night's sleep I'm wondering if I can tighten this up by just subtracting dates directly. A quickie proof-of-concept:

    Code:
    ?now()-now()
     0 
    
    ?now() - date()
     0.58405092592875 
    
    ?format(now() - date(),"h:nn")
    14:01

  6. #6
    Super Moderator si_the_geek's Avatar
    Join Date
    Jul 2002
    Location
    Bristol, UK
    Posts
    41,974

    Re: Elegant code for TimeSince()

    With that you would still need to do a similar amount of work to determine which format to use, so you wouldn't gain much.

    While there would be a minor speed boost for the initial calculation, the display would probably be slower.

    You could possibly re-map things to your previous calculations, but that is cumbersome so I don't think you'd gain much there either - except more complicated code.

    Quote Originally Posted by Ellis Dee View Post
    My function is obviously not very efficient, but I've avoided the Mod operator for years just on principle after learning on these forums that it executes (comparatively) slowly. Do you know if Mod is noticeable slower than my multiplication alternative?
    I don't know, as it has been too long since I regularly worked with VB6... but I suspect that your multiplication alternative would be faster.

  7. #7
    PowerPoster dilettante's Avatar
    Join Date
    Feb 2006
    Posts
    24,487

    Re: Elegant code for TimeSince()

    Mod is not slow, certainly no slower than alternative code... the rare case being for whole powers of two where you could use And. Any "slowness" comes from its use of an integer divide.

    You need to stop listening to those who mislead you.

    How many millions of these intervals are you trying to format anyway?

  8. #8

    Thread Starter
    PowerPoster Ellis Dee's Avatar
    Join Date
    Mar 2007
    Location
    New England
    Posts
    3,530

    Re: Elegant code for TimeSince()

    Quote Originally Posted by dilettante View Post
    How many millions of these intervals are you trying to format anyway?
    Three! Not million, I mean three total, as I have three flash drives I use for rolling backups. Obviously efficiency doesn't matter here; I've just always avoided it on principle.



    Going further into a subtraction method I see that a datetime value of 0 is represented as #12/30/1899 12:00:00 AM#, which throws a bit of a wrench into my plans. What about the crusades? The renaissance? Microsoft has no sense of history.

    Anyway, my initial effort:
    vb Code:
    1. Public Function TimeSince1(pdtmDate As Date) As String
    2.     Dim strUnit() As String
    3.     Dim strTerm() As String
    4.     Dim i As Long
    5.    
    6.     strTerm = Split("year month day hour minute second", " ")
    7.     strUnit = Split(Format(Now() - pdtmDate, "yyyy m d h n s"), " ")
    8.     For i = 0 To 5
    9.         Debug.Print Plural(CLng(strUnit(i)), strTerm(i))
    10.     Next
    11. End Function

    Which generates:
    Code:
    timesince1 #3/7/2016#
    1899 years
    12 months
    30 days
    14 hours
    31 minutes
    59 seconds
    I'm thinking if I then subtract the real NullDate values, I might be getting somewhere.

  9. #9
    Super Moderator si_the_geek's Avatar
    Join Date
    Jul 2002
    Location
    Bristol, UK
    Posts
    41,974

    Re: Elegant code for TimeSince()

    That certainly has potential for making it shorter... but the months wont be reliable (nor will leap years), as you are effectively resetting it so that it will always count the first month as being 31 days (and the second as 29 I think, then 31 for the third, etc).

  10. #10

    Thread Starter
    PowerPoster Ellis Dee's Avatar
    Join Date
    Mar 2007
    Location
    New England
    Posts
    3,530

    Re: Elegant code for TimeSince()

    Using the real null date of #12/31/1899 12:00:00 AM# proved unhelpful, but my small brain was able to wrap my head around using #1/1/2000# as a simpler "zero."
    vb Code:
    1. Public Function TimeSince(pdtmDate As Date) As String
    2.     Dim strUnit() As String
    3.     Dim strTerm() As String
    4.     Dim strNull() As String
    5.     Dim i As Long
    6.    
    7.     strTerm = Split("year month day hour minute second", " ")
    8.     strUnit = Split(Format(Now() - pdtmDate + #1/1/2000#, "yyyy m d h n s"), " ")
    9.     strNull = Split(Format(#1/1/2000#, "yyyy m d h n s"), " ")
    10.     For i = 0 To 5
    11.         If Val(strUnit(i) - strNull(i)) > 0 Then Exit For
    12.     Next
    13.     Select Case i
    14.         Case 0 To 4: TimeSince = Plural(CLng(strUnit(i) - strNull(i)), strTerm(i)) & ", " & Plural(CLng(strUnit(i + 1) - strNull(i + 1)), strTerm(i + 1)) & " ago"
    15.         Case 5: TimeSince = Plural(CLng(strUnit(i) - strNull(i)), strTerm(i)) & " ago"
    16.         Case 6: TimeSince = "0 seconds ago"
    17.     End Select
    18. End Function

    You're right about the month weirdness, and it abandons the concept of weeks entirely, but other than x = (31 - days in current month) days ago this appears to work:

    Code:
    ?timesince(Now())
    0 seconds ago
    
    ?timesince(#3/7/2016 3:18 PM#)
    39 seconds ago
    
    ?timesince(#3/7/2016 3:13 PM#)
    6 minutes, 21 seconds ago
    
    ?timesince(#3/4/2016 2:13 PM#)
    3 days, 1 hour ago
    
    ?timesince(#2/4/2016#)
    1 month, 1 day ago <==== WRONG!!!
    
    ?timesince(#1/4/2016#)
    2 months, 3 days ago
    
    ?timesince(#1/17/2015#)
    1 year, 1 month ago
    Last edited by Ellis Dee; Mar 7th, 2016 at 03:27 PM.

  11. #11

    Thread Starter
    PowerPoster Ellis Dee's Avatar
    Join Date
    Mar 2007
    Location
    New England
    Posts
    3,530

    Re: Elegant code for TimeSince()

    Success!

    I added the ability to specify an end date, and removed the " ago" from the return value. It now tells you how much time elapsed between any two date/times, even ones before or on both sides of my arbitrary 1/1/2000 "null date."

    The month issue only appeared if Start is in the month before End and Start's day of month is equal to or less than End's. This issue is corrected in the first line of code.

    I kind of went nuts optimizing the function for fewest lines of code, to the point that it's no longer particularly readable. But at only 12 lines of code for the core logic, I'm quite pleased.

    vb Code:
    1. Public Function TimeBetween(ByVal pdtmStart As Date, ByVal pdtmEnd As Date) As String
    2.     Dim strSplit() As String
    3.     Dim i As Long
    4.    
    5.     If DateDiff("m", pdtmStart, pdtmEnd) = 1 And Day(pdtmStart) <= Day(pdtmEnd) Then pdtmEnd = DateAdd("d", 31 - Day(DateSerial(Year(pdtmStart), Month(pdtmStart) + 1, 0)), pdtmEnd)
    6.     strSplit = Split(Format(pdtmEnd - pdtmStart + #1/1/2000#, "yyyy m d h n s ") & Format(#1/1/2000#, "yyyy m d h n s ") & "year month day hour minute second", " ")
    7.     For i = 0 To 5
    8.         If Val(strSplit(i) - strSplit(i + 6)) > 0 Or i = 5 Then Exit For
    9.     Next
    10.     TimeBetween = Plural(CLng(strSplit(i) - strSplit(i + 6)), strSplit(i + 12))
    11.     If i < 5 Then TimeBetween = TimeBetween & ", " & Plural(CLng(strSplit(i + 1) - strSplit(i + 7)), strSplit(i + 13))
    12. End Function
    13.  
    14. Private Function Plural(plngNumber As Long, pstrSingular As String) As String
    15.     If plngNumber = 1 Then Plural = "1 " & pstrSingular Else Plural = plngNumber & " " & pstrSingular & pstrPlural
    16. End Function

  12. #12
    Super Moderator si_the_geek's Avatar
    Join Date
    Jul 2002
    Location
    Bristol, UK
    Posts
    41,974

    Re: [RESOLVED] Elegant code for TimeSince()

    I'm not sure it is quite right, here are two of my tests that don't give the answers I'd expect:
    ? TimeBetween(#3/2/2014#, #3/1/2014#)
    11 months, 30 days

    ? TimeBetween(#3/2/2014#, #5/1/2014#)
    2 months, 0 days

  13. #13

    Thread Starter
    PowerPoster Ellis Dee's Avatar
    Join Date
    Mar 2007
    Location
    New England
    Posts
    3,530

    Re: [RESOLVED] Elegant code for TimeSince()

    Well, shoot. Let me take a look.

    Note that in your first example, Start is after End. That's not supported.

    Your second example looks like an issue, though.

  14. #14

    Thread Starter
    PowerPoster Ellis Dee's Avatar
    Join Date
    Mar 2007
    Location
    New England
    Posts
    3,530

    Re: Elegant code for TimeSince()

    I was unable to figure out how to account for the root problem. I successfully added an exception for months = 2 to 12, but the problem persisted with years and I couldn't readily solve that one. Which meant I ended up coding special logic for both months/days and years/months, so at this point I may as well do it straight up like in the OP since it's easier to maintain and includes weeks.

    I compressed the code from the OP down, but not so much that it's hard to decipher. This is what I'm left with:

    vb Code:
    1. Public Const NullDate As Date = #12:00:00 AM#
    2.  
    3. Public Function TimeSince(pdtmDate As Date) As String
    4.     Dim lngYears As Long
    5.     Dim lngMonths As Long
    6.     Dim lngWeeks As Long
    7.     Dim lngDays As Long
    8.     Dim lngHours As Long
    9.     Dim lngMinutes As Long
    10.     Dim lngSeconds As Long
    11.    
    12.     If pdtmDate = NullDate Then
    13.         TimeSince = "Never"
    14.         Exit Function
    15.     End If
    16.     lngSeconds = DateDiff("s", pdtmDate, Now())
    17.     If lngSeconds <= 60 Then ' Seconds
    18.         TimeSince = Plural(lngSeconds, "second")
    19.     ElseIf Reduce(lngMinutes, lngSeconds, 60) <= 60 Then ' Minutes, Seconds
    20.         TimeSince = Plural(lngMinutes, "minute") & ", " & Plural(lngSeconds, "second") & " ago"
    21.     ElseIf Reduce(lngHours, lngMinutes, 60) <= 24 Then ' Hours, Minutes
    22.         TimeSince = Plural(lngHours, "hour") & ", " & Plural(lngMinutes, "minute") & " ago"
    23.     ElseIf Reduce(lngDays, lngHours, 24) <= 7 Then ' Days, Hours
    24.         TimeSince = Plural(lngDays, "day") & ", " & Plural(lngHours, "hour") & " ago"
    25.     ElseIf pdtmDate >= DateAdd("d", -56, Now()) Then ' Weeks, Days
    26.         Reduce lngWeeks, lngDays, 7
    27.         TimeSince = Plural(lngWeeks, "week") & ", " & Plural(lngDays, "day") & " ago"
    28.     Else
    29.         lngMonths = DateDiff("m", pdtmDate, Now())
    30.         If Day(pdtmDate) > Day(Now()) Then lngMonths = lngMonths - 1
    31.         If lngMonths <= 12 Then ' Months, Weeks
    32.             lngWeeks = DateDiff("d", pdtmDate, DateAdd("m", -lngMonths, Now())) \ 7
    33.             TimeSince = Plural(lngMonths, "month") & ", " & Plural(lngWeeks, "week") & " ago"
    34.         ElseIf Reduce(lngYears, lngMonths, 12) < 10 Then ' Years, Months
    35.             TimeSince = Plural(lngYears, "year") & ", " & Plural(lngMonths, "month") & " ago"
    36.         Else ' Years
    37.             TimeSince = Plural(lngYears, "year") & " ago"
    38.         End If
    39.     End If
    40. End Function
    41.  
    42. Private Function Reduce(plngLarge As Long, plngSmall As Long, plngFactor As Long) As Long
    43.     plngLarge = plngSmall \ plngFactor
    44.     plngSmall = plngSmall Mod plngFactor
    45.     Reduce = plngLarge
    46. End Function
    47.  
    48. Private Function Plural(plngNumber As Long, pstrSingular As String, Optional pstrPlural As String = "s") As String
    49.     If plngNumber = 1 Then Plural = "1 " & pstrSingular Else Plural = plngNumber & " " & pstrSingular & pstrPlural
    50. End Function

    38 lines of code for the core logic, which makes me sad. I was so happy with the 12-line solution. Too bad I can't figure out how to make it work.
    Last edited by Ellis Dee; Mar 7th, 2016 at 07:59 PM.

  15. #15
    Super Moderator si_the_geek's Avatar
    Join Date
    Jul 2002
    Location
    Bristol, UK
    Posts
    41,974

    Re: Elegant code for TimeSince()

    Quote Originally Posted by Ellis Dee View Post
    Note that in your first example, Start is after End. That's not supported.
    That's fair enough, it's a very easy fix anyway (for others reading later: just swap the dates), I just thought I'd mention it in case you'd had successful tests.

    Quote Originally Posted by Ellis Dee View Post
    I was unable to figure out how to account for the root problem. I successfully added an exception for months = 2 to 12, but the problem persisted with years and I couldn't readily solve that one. Which meant I ended up coding special logic for both months/days and years/months, so at this point I may as well do it straight up like in the OP since it's easier to maintain and includes weeks.
    I haven't thought of a clever fix for the previous code (just the kind of thing you would have tried), so that's probably the way to go.

    Keeping code short is nice, but correctness and maintainability are more important.

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •  



Click Here to Expand Forum to Full Width