Skip to content

Instantly share code, notes, and snippets.

@favila
Last active October 14, 2025 18:00
Show Gist options
  • Save favila/fc06864c44ea6881f3f94748afa0db66 to your computer and use it in GitHub Desktop.
Save favila/fc06864c44ea6881f3f94748afa0db66 to your computer and use it in GitHub Desktop.
localdatetime-encoding: Encode year-month-day with optional hour-minute-second values (LocalDate and LocalDateTime) into the 57 low bits of a long for storage.
(ns favila.localdatetime-encoding
"Encode year-month-day with optional hour-minute-second values
(LocalDate and LocalDateTime)
into the 57 low bits of a long for storage.
See #'datetime->long for encoding details.
See #'encode to encode. Supports java.time.LocalDate and java.time.LocalDateTime,
extensible via EncodeLocalDateTime protocol.
See #'decode-localdate-or-time and variants to decode to LocalDate or LocalDateTime."
(:import [java.time LocalDate LocalDateTime LocalTime]))
(defn datetime->long
"Given a LocalDate epoch day and LocalTime second of day or -1 for no time,
return a long >= 0 of 57 significant binary digits which encodes both.
The long will collate in the same way as a tuple [epoch-day, second-of-day].
This is meant to collate the same way as equivalent LocalDateTime
or LocalDate, where the LocalDate sorts before all LocalDateTimes on the same
day."
^long [^long epoch-day ^long second-of-day]
{:pre [(<= -365243219162 epoch-day 365241780471)
(<= -1 second-of-day 86399)]}
(bit-or
(bit-shift-left (+ epoch-day 365243219162) 17)
(inc second-of-day)))
(defprotocol EncodeLocalDateTime
(-encode ^long [local-date-or-datetime] "Return date encoded as a datetime->long"))
(extend-protocol EncodeLocalDateTime
LocalDate
(-encode [o] (datetime->long (.toEpochDay o) -1))
LocalDateTime
(-encode [o] (datetime->long (-> (.toLocalDate o) (.toEpochDay))
(-> (.toLocalTime o) (.toSecondOfDay)))))
(defn long->epoch-day
"Return the epoch-day component of a long produced by datetime->long."
^long [^long datetime-long]
{:post [(<= -365243219162 % 365241780471)]}
(- (bit-shift-right datetime-long 17) 365243219162))
(defn long->second-of-day
"Return the second-of-day component of a long produced by datetime->long.
Returns -1 if the long has no time component (i.e. encodes a LocalDate)."
^long [^long datetime-long]
{:post [(<= -1 % 86399)]}
(dec (bit-and 0x1ffff datetime-long)))
(defn encode
"Encode an object into an encoded-date long.
The object must represent a year-month-day value, optionally with hour-minute-seconds."
^long [x]
(-encode x))
(defn decode-localdate-or-time
"Decodes a long from #'encode to either a j.time.LocalDateTime
or to a j.time.LocalDate if there was no time component."
[^long encoded-date]
(let [ld (LocalDate/ofEpochDay (long->epoch-day encoded-date))
t (long->second-of-day encoded-date)]
(if (== -1 t)
ld
(LocalDateTime/of ld (LocalTime/ofSecondOfDay t)))))
(defn decode-localdatetime
"Decodes a long from #'encode to a j.time.LocalDateTime.
Throws if the long has no time component."
^LocalDateTime [^long encoded-date]
;; faster than instance-of check
(when (== -1 (long->second-of-day encoded-date))
(throw (IllegalArgumentException.
"Provided encoded-date has no time component.")))
(decode-localdate-or-time encoded-date))
(defn decode-localdate
"Decodes a long from #'encode to a j.time.LocalDate.
Throws if the long has a time component."
^LocalDate [^long encoded-date]
;; faster than instance-of check
(when-not (== -1 (long->second-of-day encoded-date))
(throw (IllegalArgumentException.
"Provided encoded-date has a time component.")))
(decode-localdate-or-time encoded-date))
(defn decode-coerce-localdatetime
"Decodes a long from #'encode to a j.time.LocalDateTime.
Coerces an encoded-date without a time component (i.e. a LocalDate)
to a LocalDateTime with a time component at the first second of the day.
Note that this coercion means the encoded-date value will not round-trip!"
^LocalDateTime [^long encoded-date]
(let [ld (LocalDate/ofEpochDay (long->epoch-day encoded-date))
lt (LocalTime/ofSecondOfDay (max 0 (long->second-of-day encoded-date)))]
(LocalDateTime/of ld lt)))
;; testing
(comment
(defn roundtrips
"Test that datetime->long and encoded components roundtrip."
[^long epoch-day ^long second-of-day]
(let [x (datetime->long epoch-day second-of-day)]
(and
(== epoch-day (long->epoch-day x))
(== second-of-day (long->second-of-day x)))))
(assert
(every? true?
[(roundtrips -365243219162 -1)
(roundtrips 0 -1)
(roundtrips 365241780471 -1)
(roundtrips -365243219162 0)
(roundtrips 365241780471 86399)
(< (datetime->long -365243219162 -1) (datetime->long -365243219162 0))
(< (datetime->long 0 -1) (datetime->long 0 0))
(< (datetime->long 365241780471 -1) (datetime->long 365241780471 0))]))
(datetime->long -365243219162 -1)
;=> 0
(datetime->long 365241780471 86399)
;=> 95746129871982976
;; 57 significant binary digits
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment