A time library that thinks in numbers and embraces functional programming.
Timing offers a different approach to time computation by working in the numeric domain first. If you enjoy functional programming, sequences, and immutable data, you might find Timing's approach refreshing.
- Pure Clojure/ClojureScript with no external dependencies
- Cross-platform compatibility between JVM and JavaScript
- Immutable operations that play nicely with functional code
- Support for multiple calendar systems (Gregorian, Julian, Hebrew, Islamic)
- Holiday awareness for ~200 countries
Instead of working with date objects, Timing encourages you to:
;; Work with time as numbers (milliseconds since epoch)
(def now (time->value (date 2024 6 15 14 30 0))) ; => 1718461800000
(def later (+ now (days 7) (hours 3))) ; Simple arithmetic!
(value->time later) ; Back to Date when needed
; => #inst "2024-06-22T17:30:00.000-00:00"
Timing was built to work well with Clojure's sequence operations:
;; Generate quarterly dates for 2024
(->> (range 0 12 3)
(map #(add-months (time->value (date 2024 1 1)) %))
(map value->time))
; => (#inst "2024-01-01T00:00:00.000-00:00"
; #inst "2024-04-01T00:00:00.000-00:00"
; #inst "2024-07-01T00:00:00.000-00:00"
; #inst "2024-10-01T00:00:00.000-00:00")
;; Business days using familiar sequence functions
(def q1 (time->value (date 2024 1 15)))
(->> (business-days-in-range (start-of-quarter q1) (end-of-quarter q1))
(take 5)
(map value->time))
; => (#inst "2024-01-01T23:00:00.000-00:00"
; #inst "2024-01-02T23:00:00.000-00:00"
; #inst "2024-01-03T23:00:00.000-00:00"
; #inst "2024-01-04T23:00:00.000-00:00"
; #inst "2024-01-07T23:00:00.000-00:00")
Handle edge cases naturally with smart period functions:
;; Fixed-length periods (traditional)
(+ today (days 30) (hours 8))
;; Variable-length periods (handles month/year complexities)
(-> today
(add-months 3) ; Handles month lengths properly
(add-years 2) ; Handles leap years automatically
(+ (days 15))) ; Mix with fixed periods seamlessly
;; deps.edn
{:deps {dev.gersak/timing {:mvn/version "0.7.0"}}}
;; Leiningen
[dev.gersak/timing "0.7.0"]
(require '[timing.core :as t])
;; Create dates
(def birthday (t/date 1990 5 15))
(def now (t/date))
;; Convert to numeric domain for computation
(def age-ms (- (t/time->value now) (t/time->value birthday)))
(def age-days (/ age-ms t/day))
;; Time arithmetic
(def next-week (+ (t/time->value now) (t/days 7)))
(def next-month (t/add-months (t/time->value now) 1))
;; Convert back to dates
(t/value->time next-week)
; => #inst "2025-06-08T13:56:08.098-00:00"
(t/value->time next-month)
; => #inst "2025-07-01T13:56:08.098-00:00"
;; All time units as precise numbers
t/millisecond ; => 1
t/second ; => 1000
t/minute ; => 60000
t/hour ; => 3600000
t/day ; => 86400000
t/week ; => 604800000
;; Helper functions
(t/days 7) ; => 604800000
(t/hours 3) ; => 10800000
(t/minutes 45) ; => 2700000
;; Variable-length periods with edge case handling
(t/add-months (t/time->value (t/date 2024 1 31)) 1)
; => Converts Jan 31 -> Feb 28, 2024 (handles month-end properly)
(t/add-months (t/time->value (t/date 2023 1 31)) 1)
; => Converts Jan 31 -> Feb 27, 2023 (non-leap year handling)
(t/add-years (t/time->value (t/date 2024 2 29)) 1)
; => Feb 29 -> Feb 27, 2025 (Feb 29 doesn't exist in 2025)
;; Chain operations naturally
(-> (t/time->value (t/date 2024 1 15))
(t/add-months 6)
(t/add-years 2)
(+ (t/days 10))
(+ (t/hours 8))
t/value->time)
; => #inst "2026-07-25T06:00:00.000-00:00"
;; Round to any precision
(t/round-number 182.8137 0.25 :up) ; => 183.0
(t/round-number 182.8137 0.25 :down) ; => 182.75
(t/round-number 182.8137 0.25 :ceil) ; => 183.0
(t/round-number 182.8137 0.25 :floor) ; => 182.75
;; Align to time boundaries
(def test-value (t/time->value (t/date 2024 6 15 14 30 45)))
(t/value->time (t/midnight test-value)) ; Round to start of day
; => #inst "2024-06-14T22:00:00.000-00:00"
(t/value->time (t/round-number test-value t/hour :floor)) ; Round to start of hour
; => #inst "2024-06-15T12:00:00.000-00:00"
(t/day-time-context (t/time->value (t/date 2024 6 15)))
; => {:leap-year? true,
; :day 6,
; :hour 0,
; :week 24,
; :weekend? true,
; :days-in-month 30,
; :first-day-in-month? false,
; :second 0,
; :days-in-year 366,
; :value 1718409600000,
; :month 6,
; :year 2024,
; :millisecond 0,
; :holiday? false,
; :last-day-in-month? false,
; :day-in-month 15,
; :minute 0}
;; Get all days in a month (helpful for UI calendars)
(take 3 (t/calendar-frame (t/time->value (t/date 2024 6 1)) :month))
; => ({:day 6, :week 22, :first-day-in-month? true, :value 1717200000000,
; :month 6, :year 2024, :last-day-in-month? false, :weekend true, :day-in-month 1}
; {:day 7, :week 22, :first-day-in-month? false, :value 1717286400000,
; :month 6, :year 2024, :last-day-in-month? false, :weekend true, :day-in-month 2}
; {:day 1, :week 23, :first-day-in-month? false, :value 1717372800000,
; :month 6, :year 2024, :last-day-in-month? false, :weekend false, :day-in-month 3})
;; Also available: :year and :week views
(require '[timing.adjusters :as adj])
;; Navigate to specific days
(def today (t/time->value (t/date 2024 6 15))) ; Saturday
(adj/next-day-of-week today 1) ; Next Monday
; => 1718496000000 (converts to #inst "2024-06-16T22:00:00.000-00:00")
(adj/first-day-of-month-on-day-of-week today 5) ; First Friday of month
; => 1717716000000 (converts to #inst "2024-06-06T22:00:00.000-00:00")
(adj/last-day-of-month-on-day-of-week today 5) ; Last Friday of month
; => 1719525600000 (converts to #inst "2024-06-27T22:00:00.000-00:00")
(adj/nth-day-of-month-on-day-of-week today 2 3) ; 3rd Tuesday of month
; => 1718582400000 (converts to #inst "2024-06-17T22:00:00.000-00:00")
;; Period boundaries
(adj/start-of-week today) ; Start of current week
(adj/end-of-month today) ; End of current month
(adj/start-of-quarter today) ; Start of current quarter
(adj/end-of-year today) ; End of current year
;; Business day operations
(adj/next-business-day today) ; Skip weekends
(adj/add-business-days today 5) ; Add 5 business days
; => 1718928000000 (converts to #inst "2024-06-20T22:00:00.000-00:00")
(take 3 (map t/value->time (adj/business-days-in-range
(adj/start-of-month today)
(adj/end-of-month today))))
; => (#inst "2024-06-02T22:00:00.000-00:00"
; #inst "2024-06-03T22:00:00.000-00:00"
; #inst "2024-06-04T22:00:00.000-00:00")
(require '[timing.util :as util])
(util/print-calendar 2024 6)
; Prints:
; June 2024
; +---+---+---+---+---+---+---+
; |Mon|Tue|Wed|Thu|Fri|Sat|Sun|
; +---+---+---+---+---+---+---+
; | | | | | | 1 | 2 |
; | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
; |10 |11 |12 |13 |14 |15 |16 |
; |17 |18 |19 |20 |21 |22 |23 |
; |24 |25 |26 |27 |28 |29 |30 |
; +---+---+---+---+---+---+---+
;; Customizable options available
(util/print-calendar 2024 6 {:first-day-of-week 7 ; Sunday first
:show-week-numbers true ; Show week numbers
:day-width 4}) ; Wider cells
;; Print entire year
(util/print-year-calendar 2024)
;; Dynamic timezone context
(t/with-time-configuration {:timezone "America/New_York"}
(select-keys (t/day-time-context (t/time->value (t/date 2024 6 15)))
[:year :month :day-in-month :hour]))
; => {:year 2024, :month 6, :day-in-month 15, :hour 0}
;; Convert between timezones
(def my-time (t/time->value (t/date 2024 6 15 12 0 0)))
(def london-time (t/teleport my-time "Europe/London"))
(t/value->time london-time)
; => #inst "2024-06-15T09:00:00.000-00:00" (adjusted for timezone)
;; Custom weekend days and holidays
(t/with-time-configuration {:weekend-days #{5 6} ; Fri/Sat weekend
:holiday? my-holiday-fn} ; Custom holiday logic
(t/weekend? (t/time->value (t/date 2024 6 14)))) ; Friday
; => true
;; Switch calendar systems dynamically
(let [now (t/time->value (t/date 2024 6 15))]
(println "Gregorian:"
(select-keys (t/day-time-context now) [:year :month :day-in-month]))
(println "Hebrew:"
(t/with-time-configuration {:calendar :hebrew}
(select-keys (t/day-time-context now) [:year :month :day-in-month])))
(println "Islamic:"
(t/with-time-configuration {:calendar :islamic}
(select-keys (t/day-time-context now) [:year :month :day-in-month]))))
; Prints:
; Gregorian: {:year 2024, :month 6, :day-in-month 15}
; Hebrew: {:year 5784, :month 3, :day-in-month 9}
; Islamic: {:year 1445, :month 12, :day-in-month 8}
;; Available calendars: :gregorian, :julian, :hebrew, :islamic
(require '[timing.holiday :as holiday])
;; Note: For full holiday support, also require:
;; (require '[timing.holiday.all]) ; Add all holiday implementations
;; Check holidays by country
(holiday/? :us (t/time->value (t/date 2024 7 4))) ; => holiday map
(holiday/? :us (t/time->value (t/date 2024 12 25))) ; => holiday map
(holiday/? :us (t/time->value (t/date 2024 1 1))) ; => holiday map
;; Get holiday name (important: use holiday/name function!)
(def july4-holiday (holiday/? :us (t/time->value (t/date 2024 7 4))))
(holiday/name :en july4-holiday) ; => "Independence Day"
(def christmas-holiday (holiday/? :us (t/time->value (t/date 2024 12 25))))
(holiday/name :en christmas-holiday) ; => "Christmas Day"
;; Supports ~200 countries
(require '[timing.cron :as cron])
;; Parse and work with cron expressions
(def next-noon (cron/next-timestamp (t/time->value (t/date 2024 6 15)) "0 0 12 * * ?"))
(t/value->time next-noon)
; => #inst "2024-06-15T10:00:00.000-00:00" (next occurrence of daily noon)
(cron/valid-timestamp? (t/time->value (t/date 2024 6 15 12 0 0)) "0 0 12 * * ?")
; => true (matches cron pattern)
;; Generate future execution times
(def start-time (t/time->value (t/date 2024 6 15)))
(take 3 (map t/value->time (cron/future-timestamps start-time "0 0 9 * * MON")))
; => (#inst "2024-06-17T07:00:00.000-00:00" ; Next Monday 9 AM
; #inst "2024-06-24T07:00:00.000-00:00" ; Following Monday
; #inst "2024-07-01T07:00:00.000-00:00") ; And the one after
;; Add 30 business days to today
(def deadline
(adj/add-business-days (t/time->value (t/date 2024 6 15)) 30))
(t/value->time deadline)
; => #inst "2024-07-25T22:00:00.000-00:00"
;; Find all month-end Fridays in 2024
(def month-end-fridays
(->> (range 1 13)
(map #(t/time->value (t/date 2024 % 1)))
(map #(adj/last-day-of-month-on-day-of-week % 5))
(map t/value->time)))
(take 3 month-end-fridays)
; => (#inst "2024-01-25T23:00:00.000-00:00"
; #inst "2024-02-22T23:00:00.000-00:00"
; #inst "2024-03-28T23:00:00.000-00:00")
;; Calculate working days between two dates
(def working-days
(count (adj/business-days-in-range
(t/time->value (t/date 2024 6 1))
(t/time->value (t/date 2024 6 30)))))
; => 20 (working days in June 2024)
;; Every 2nd Tuesday for next 6 months
(def bi-weekly-meetings
(->> (adj/every-nth-day-of-week today 2 2) ; Every 2nd Tuesday
(take-while #(< % (t/add-months today 6)))
(take 12)
(map t/value->time)))
;; Quarterly board meetings (last Friday of quarter)
(def quarterly-meetings
(->> [3 6 9 12] ; End of quarters
(map #(t/time->value (t/date 2024 % 1)))
(map adj/end-of-month)
(map #(adj/last-day-of-month-on-day-of-week % 5))
(map t/value->time)))
;; Monthly payment dates (15th of each month)
(def payment-dates-2024
(->> (range 1 13)
(map #(t/time->value (t/date 2024 % 15)))
(map #(if (adj/weekend? %)
(adj/previous-business-day %) ; Move to Friday if weekend
%))
(map t/value->time)))
;; Quarter-end reporting dates
(def quarter-ends
(->> (range 2024 2027)
(mapcat #(map (fn [q] (adj/end-of-quarter
(t/time->value (t/date % (* q 3) 1))))
[1 2 3 4]))
(map t/value->time)))
timing/
βββ core/ # Core time computation (timing.core)
βββ timezones/ # IANA timezone database (timing.timezones)
βββ holidays/ # Country-specific holidays (timing.holiday)
βββ cron/ # Cron scheduling (timing.cron)
βββ util/ # Utility functions (timing.util, timing.adjusters)
- Numeric Domain First - Computation in milliseconds, objects for display
- Immutable Values - All operations return new values
- Functional Composition - Everything chains naturally with threading macros
- Zero Dependencies - Pure Clojure/ClojureScript
- Cross-Platform - Identical behavior on JVM and JavaScript
- Efficient - Numeric arithmetic on primitive longs
- Memory Friendly - Minimal object allocation during computation
- Lazy-Friendly - Works well with lazy sequences
- Composable - Easy to combine with other functional operations
Timing might be a good fit if you:
- Enjoy functional programming patterns
- Prefer working with sequences and transformations
- Want to avoid external dependencies
- Like the numeric domain approach to time
- Need cross-platform Clojure/ClojureScript compatibility
- Appreciate immutable, composable operations
Other excellent time libraries like clj-time
and Java 8 Time API excel in different areas:
- clj-time: Rich object model, extensive parsing/formatting
- Java 8 Time API: Comprehensive feature set, strong typing
- js-joda: JavaScript port of JSR-310 with excellent browser support
Each approach has its strengths, and the best choice depends on your specific needs and preferences.
(def employees [{:hire-date (t/date 2023 1 15)}
{:hire-date (t/date 2023 3 20)}
{:hire-date (t/date 2023 6 10)}])
(->> employees
(map :hire-date)
(map t/time->value)
(map #(t/add-years % 1)) ; One year anniversary
(map #(adj/next-day-of-week % 5)) ; Move to Friday
(map t/value->time) ; Back to dates
(take 3))
; => (#inst "2024-01-18T23:00:00.000-00:00"
; #inst "2024-03-21T23:00:00.000-00:00"
; #inst "2024-06-13T22:00:00.000-00:00")
(-> (t/date 2024 1 1)
t/time->value
(t/add-months 6)
(adj/start-of-quarter)
(adj/next-business-day)
t/value->time)
; => #inst "2024-07-01T22:00:00.000-00:00"
;; Generate all Mondays in 2024
(take-while #(< % (t/time->value (t/date 2025 1 1)))
(adj/every-nth-day-of-week (t/time->value (t/date 2024 1 1)) 1 1))
;; All business days in a month
(def today (t/time->value (t/date 2024 6 15)))
(adj/business-days-in-range (adj/start-of-month today) (adj/end-of-month today))
- Stay in Numeric Domain - Minimize conversions to/from Date objects
- Embrace Lazy Sequences - Let Clojure's laziness work for you
- Batch Operations - Process collections functionally
- Cache Computations - Store frequently used values
;; Efficient: Stay numeric
(map #(+ % (t/days 1)) timestamps)
;; Less efficient: Convert back and forth
(map #(t/value->time (+ (t/time->value %) (t/days 1))) dates)
- Add Timing to your project
- Start with basic date arithmetic
- Explore calendar frames for UI components
- Add temporal adjusters for complex logic
- Use holidays and timezones as needed
;; Your first Timing program
(require '[timing.core :as t])
(require '[timing.adjusters :as adj])
(def today (t/time->value (t/date)))
(def next-friday
(-> today
(t/midnight)
(adj/next-day-of-week 5)
(+ (t/hours 17)))) ; 5 PM
(println "Next Friday at 5 PM:" (t/value->time next-friday))
Due to timezone handling, dates may display with timezone offsets. This is normal and expected behavior:
(t/value->time (t/time->value (t/date 2024 6 15)))
; => #inst "2024-06-14T22:00:00.000-00:00" (with timezone offset)
Holiday functions return holiday objects that need to be processed with holiday/name
:
;; Don't expect direct string results
(holiday/? :us (t/time->value (t/date 2024 7 4)))
; => {:name #function, ...}
;; Use holiday/name to get readable names
(def holiday-obj (holiday/? :us (t/time->value (t/date 2024 7 4))))
(holiday/name :en holiday-obj) ; => "Independence Day"
The round-number
function behavior varies by strategy:
(t/round-number 182.8137 0.25 :up) ; => 183.0
(t/round-number 182.8137 0.25 :down) ; => 182.75
(t/round-number 182.8137 0.25 :ceil) ; => 183.0
(t/round-number 182.8137 0.25 :floor) ; => 182.75
Copyright Β© 2018 Robert Gersak
Released under the MIT license.
Timing: A friendly approach to time computation in Clojure.