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
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
ZonedDateTime and no absolute
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.Timealways has an associated time zone that by default is UTC.
time.Timehas two destructuring functions,
Time.Clockthat 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
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 :-)