I found myself scratching my head more than a casual itch would cause when trying to calculate subscription expiration dates for an app I’m working on in Kotlin. I’ve used a similar concept in Perl a thousand times: Take the current epoch time, and add 86400 to it for each day in the future. (60 seconds in a minute, 60 minutes in an hour, 24 hours in a day = 86400 seconds per day). Works like a charm.
What is Epoch Time?
“It is the number of seconds that have elapsed since the Unix epoch, excluding leap seconds. The Unix epoch is 00:00:00 UTC on 1 January 1970 (an arbitrary date).” – Wikipedia Epoch entry
13-Digit Epoch
Kotlin throws a bit of a wrinkle in the mix by using a 13-digit epoch, which tracks not just seconds, but milliseconds. I’ll admit I googled how to get Kotlin’s epoch time and found out the hard way about the difference. After my future dates were showing up only an hour or a few minutes into the future, I dug deeper and found out about the extra 3 digits.
The fix in my case is to use 86400 * 1000 and multiply the result of that with the desired number of days in the future. Great, Simple! So why are my dates being thrown out like the results of a random number generator???
Kotlin Smart Casts
The source of my frustration could be traced back to a smart cast where I stored the number of days in the future.
// replace this with the number of days in the subscription: val interval = 365
Simple right? I never imagined that the number of days in the future would be more than 2,147,483,647, and that wasn’t really the problem. The problem didn’t occur till later when I multiplied the number of days times the number of milliseconds in a day.
val executeTime = System.currentTimeMillis() // which is the newest date, the last subscription expiration that a user already has, or today's date? // get the last subscription epoch for user, or set to 1969 with ?: 0 val lastExpirationE = subscriptionViewModel.authenticatedUser.expirationEpoch.value?.toLong() ?: 0 // the default subscription start date should be now: var subscriptionStartE = executeTime // if the user already has a subscription, use the expiration epoch as the new start date. // add one day's worth of seconds. Still in epoch time, so 86400 is the number of seconds in a day.(* 1000 to get milliseconds) if (lastExpirationE > executeTime){ subscriptionStartE = lastExpirationE + (86400 * 1000) } // subscription end date: Start date + ((number of seconds in a day 86400) * (1000 milliseconds in second) * Number of days in the subscription) val expirationEpoch = subscriptionStartE + ((86400 * 1000) * interval)
Missing Time Found
When I logged the results, I noted that lastExpirationE was 1722484800000 (Thursday, August 1, 2024, 12:00:00 AM), subscriptionStartE was 1722571200000 ( Friday, August 2, 2024, 12:00:00 AM). This tells me that 86400 * 1000 is one day’s worth of milliseconds, and the math is sound.
So why then is expirationEpoch equal to 1724042428928 (Monday, August 19, 2024, 12:40:28.928 AM)?
Because the expression ((86400 * 1000) * interval) evaluates to an Int data type because interval is an Int data type. Any value over 2,147,483,647 causes the variable to roll over like an old fashion car odometer. So my desired number of seconds to add is 31,536,000,000, clearly more than 10 times the upper limit of an Integer data type.
So Kotlin rolled the odometer over and over and wound up with the amount of 1,471,228,942, which, when added to expirationEpoch would give me, surprise, surprise: 1724042428942, just slightly off from 1724042428928. I imagine this is due to how the rolling over effect deals with the signed portion of the Integer data type, but clearly proves my entire problem could be solved by adding an L to my interval value:
// force interval to be a long data type, not an int. val interval = 365L
I never ran into this problem with Perl because all Perl variables are scalars, and the room needed to hold variables is managed automatically, and also because with Perl, one year’s worth of seconds is a lot smaller of a number than one year’s worth of milliseconds!
So, I’m left for my sage advice of the moment: “Use care in Kotlin when calculating future dates with epoch times“. Make sure any amounts you add to an epoch time are of the Long data type, just in case you edit the code later and forget to change it.