Last active
          October 14, 2025 18:00 
        
      - 
      
 - 
        
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.
  
        
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | (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