For a side project I'm writing in Go, I needed to send an email to each registered user during the night. Their night actually, according to each user's time zone. More precisely at a random time between 3am and 4am to spread email sending and avoid any rate limiting from SMTP providers or spam detection systems.
Coming from a Java/Scala background, I was looking for a large number of setters or offsetting methods on the time type. Here's what it would look like in Scala using classes in java.time
:
val tz = user.timeZone // e.g. "Europe/Paris"
// the current wall clock time in the user's time zone
val userTime = ZonedDateTime.now(ZoneId.of(tz))
// tomorrow sometime between 3am and 4am
val emailSendTime = userTime
.plusDays(1)
.withHour(3)
.plusSeconds(random.nextInt(3600))
// the instant (absolute timestamp) to be used by the task scheduler
val emailInstant = emailLocalTime.toInstant
Go puts simplicity first (too much IMHO but that's another discussion) and has only one type for time: time.Time
. No wall clock time like Java's LocalDateTime
or ZonedDateTime
and no absolute Instant
.
Similarly, time.Time
has no setter method and only one offsetting method, AddDate(years, months, days)
. So with these in hand, how do we implement the above calculation?
Go has a couple of tricks here:
time.Time
always has an associated time zone that by default is UTC.time.Time
has two destructuring functions,Time.Date()
andTime.Clock
that return the date and clock's constituents.- there is a
Date(year, month, day, hour, min, sec, nsec, location)
constructor function.
With that in hand, we can write the Go version:
location, _ := time.LoadLocation(user.TimeZone) // e.g. "Europe/Paris"
// the current wall clock time in the user's time zone
userTime := time.Now().In(location)
// destructure the time's date
year, month, day := userTime.Date()
emailSendTime := time.Date(
year, month, day+1, // date
3, 0, rand.Intn(3600), 0, // time (with nanosec)
userTime.Location())
(yes, I skipped the dreaded if err != nil
after LoadLocation
)
An interesting thing to note regarding the Date
function, is that we can pass out of range values for all parameters. For example, passing 18
for the month will set it to 6
and increment the year by one.
Nothing complicated here, but I had to "unlearn" what I knew from Java to find that a couple of simple functions were all that was needed to perform arbitrary time calculations. Now I need to look at how this works in Rust :-)