Skip to content

Repository files navigation

Timing ⏰

Clojars Project

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.

Core Philosophy:time->valuedo your thingvalue->time

Convert time to numbers, use all the arithmetic and sequence operations you love, then convert back when needed.

🤔 Why Try Timing?

Zero Dependencies, Simple Design

  • 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

Numbers-First Philosophy

Instead of working with date objects, Timing encourages you to:

;; Work with time as numbers (milliseconds since epoch)
(defnow (time->value (date202461514300))) ; => 1718461800000
(deflater (+ now (days7) (hours3))) ; Simple arithmetic!
(value->time later) ; Back to Date when needed; => #inst "2024-06-22T17:30:00.000-00:00"

Sequence-Friendly Design

Timing was built to work well with Clojure's sequence operations:

;; Generate quarterly dates for 2024
(->> (range0123)
(map #(add-months (time->value (date202411)) %))
(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")

Flexible Period Arithmetic

Handle edge cases naturally with smart period functions:

;; Fixed-length periods (traditional)
(+ today (days30) (hours8))
;; Variable-length periods (handles month/year complexities)
(-> today
(add-months3) ; Handles month lengths properly
(add-years2) ; Handles leap years automatically 
(+ (days15))) ; Mix with fixed periods seamlessly

⚡ Quick Start

Installation

;; deps.edn - Full umbrella library
{:deps {dev.gersak/timing {:mvn/version"0.8.2"}}}
;; Leiningen
[dev.gersak/timing "0.8.2"]

Modular Installation

You can also pick specific modules:

;; Core + standard timezones (current rules only)
{:deps {dev.gersak/timing.core {:mvn/version"0.8.2"}}}
;; Holidays
{:deps {dev.gersak/timing.holidays {:mvn/version"0.8.2"}}}
;; Cron expressions
{:deps {dev.gersak/timing.cron {:mvn/version"0.8.2"}}}

⚠️ Using Historical Timezones:timing.core depends on timing.timezones by default. If you need historical timezone data, you must exclude it and use timing.timezones.full instead:

{:deps {dev.gersak/timing.core {:mvn/version"0.8.2":exclusions [dev.gersak/timing.timezones]}
dev.gersak/timing.timezones.full {:mvn/version"0.8.2"}}}

Both artifacts provide the same timing.timezones.db namespace. The .full variant adds ~529KB but includes complete IANA tzdata history for accurate historical date calculations.

Basic Usage

(require '[timing.core :as t]
'[timing.adjusters :as adj])
;; Create dates
(defbirthday (t/date1990515))
(defnow (t/date))
;; Convert to numeric domain for computation
(defage-ms (- (t/time->value now) (t/time->value birthday)))
(defage-days (/ age-ms t/day))
;; Time arithmetic 
(defnext-week (+ (t/time->value now) (t/days7)))
(defnext-month (adj/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"

🎯 Core Features

1. Precision Time Units

;; 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/days7) ; => 604800000
(t/hours3) ; => 10800000 
(t/minutes45) ; => 2700000

2. Smart Period Arithmetic

;; Variable-length periods with edge case handling
(adj/add-months (t/time->value (t/date2024131)) 1) ; => Converts Jan 31 -> Feb 28, 2024 (handles month-end properly)
(adj/add-months (t/time->value (t/date2023131)) 1) ; => Converts Jan 31 -> Feb 27, 2023 (non-leap year handling)
(adj/add-years (t/time->value (t/date2024229)) 1) ; => Feb 29 -> Feb 27, 2025 (Feb 29 doesn't exist in 2025);; Chain operations naturally
(-> (t/time->value (t/date2024115))
(adj/add-months6)
(adj/add-years2) (+ (t/days10))
(+ (t/hours8))
t/value->time)
; => #inst "2026-07-25T06:00:00.000-00:00"

3. Flexible Rounding & Alignment

;; Round to any precision
(t/round-number182.81370.25:ceil) ; => 183.0 (always up)
(t/round-number182.81370.25:floor) ; => 182.75 (always down)
(t/round-number182.8750.25:up) ; => 183.0 (ties round up)
(t/round-number182.8750.25:down) ; => 182.75 (ties round down);; Align to time boundaries
(deftest-value (t/time->value (t/date2024615143045)))
(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"

4. Rich Time Context

(t/day-time-context (t/time->value (t/date2024615)))
; => {: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}

5. Calendar Frame Generation

;; Get all days in a month (helpful for UI calendars)
(take3 (t/calendar-frame (t/time->value (t/date202461)) :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

🛠️ Advanced Features

Temporal Adjusters

(require '[timing.adjusters :as adj])
;; Navigate to specific days
(deftoday (t/time->value (t/date2024615))) ; Saturday
(adj/next-day-of-week today 1) ; Next Monday; => 1718582400000 (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; => 1717718400000 (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; => 1719532800000 (converts to #inst "2024-06-27T22:00:00.000-00:00")
(adj/nth-day-of-month-on-day-of-week today 23) ; 3rd Tuesday of month; => 1718668800000 (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")
(take3 (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")

Calendar Printing

(require '[timing.util :as util])
(util/print-calendar20246)
; 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-calendar20246 {:first-day-of-week7; Sunday first:show-week-numberstrue; Show week numbers:day-width4}) ; Wider cells;; Print entire year
(util/print-year-calendar2024)

Timezone & Configuration

(require '[timing.timezones.db :as tz])
;; Dynamic timezone context
(t/with-time-configuration {:timezone"America/New_York"}
(select-keys (t/day-time-context (t/time->value (t/date2024615)))
[:year:month:day-in-month:hour]))
; => {:year 2024, :month 6, :day-in-month 15, :hour 0};; Work in UTC (ignores local timezone)
(t/with-utc
(t/day-time-context (t/time->value (t/date2024615))))
;; Convert between timezones
(defmy-time (t/time->value (t/date20246151200)))
(deflondon-time (t/teleport my-time "Europe/London"))
(t/value->time london-time)
; => #inst "2024-06-15T09:00:00.000-00:00" (adjusted for timezone);; Fuzzy timezone matching (helpful suggestions on typos)
(tz/get-timezone"NewYork") ; Throws with suggestion: "America/New_York";; Custom weekend days and holidays
(t/with-time-configuration {:weekend-days #{56} ; Fri/Sat weekend:holiday? my-holiday-fn} ; Custom holiday logic
(t/weekend? (t/time->value (t/date2024614)))) ; Friday; => true

Historical Timezone Support

With timing.timezones.full, you can look up timezone rules as they existed at any point in history:

(require '[timing.timezones.db :as tz])
;; 2-arity get-timezone returns historical rules
(defts-1943 (t/time->value (t/utc-date1943615)))
(defts-2023 (t/time->value (t/utc-date2023615)))
(tz/get-timezone"Europe/Belgrade" ts-1943) ; WWII era rules (C-Eur)
(tz/get-timezone"Europe/Belgrade" ts-2023) ; Modern EU rules;; 1-arity returns current rules only
(tz/get-timezone"Europe/Belgrade") ; Always returns current rules;; Check if historical data is available
(contains? (get-in tz/db [:zones"Europe/Belgrade"]) :history)
; => true (with timing.timezones.full); => false (with timing.timezones)

Multiple Calendar Systems

;; Switch calendar systems dynamically
(let [now (t/time->value (t/date2024615))]
(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

Holiday Integration

Holiday data is loaded on-demand. Cherry-pick only the countries you need to minimize bundle size, or load all ~200 countries at once:

(require '[timing.holiday :as holiday])
;; Option 1: Cherry-pick specific countries (recommended for smaller bundles)
(require '[timing.holiday.us]) ; Load US holidays
(require '[timing.holiday.pl]) ; Load Polish holidays
(require '[timing.holiday.de]) ; Load German holidays;; Option 2: Load ALL countries (~200 locales)
(require '[timing.holiday.all])
;; Check if a date is a holiday
(holiday/?:us (t/time->value (t/date202474))) ; => holiday map or nil
(holiday/?:pl (t/time->value (t/date20241111))) ; => Polish Independence Day;; Get holiday name (use holiday/name function)
(defjuly4 (holiday/?:us (t/time->value (t/date202474))))
(holiday/name:en july4) ; => "Independence Day";; Localized names (when available)
(defchristmas (holiday/?:pl (t/time->value (t/date20241225))))
(holiday/name:pl christmas) ; => "Boże Narodzenie"
(holiday/name:en christmas) ; => "Christmas Day"

Cron Expression Parser

(require '[timing.cron :as cron])
;; Parse and work with cron expressions
(defnext-noon (cron/next-timestamp (t/time->value (t/date2024615)) "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/date20246151200)) "0 0 12 * * ?")
; => true (matches cron pattern);; Generate future execution times
(defstart-time (t/time->value (t/date2024615)))
(take3 (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;; Standard 6-field cron format: second minute hour day-of-month month day-of-week;; Supports: ranges (1-5), lists (1,3,5), steps (*/15), names (MON, JAN), L/W modifiers

💡 Real-World Examples

Business Date Calculations

;; Add 30 business days to today
(defdeadline (adj/add-business-days (t/time->value (t/date2024615)) 30))
(t/value->time deadline)
; => #inst "2024-07-25T22:00:00.000-00:00";; Find all month-end Fridays in 2024
(defmonth-end-fridays
(->> (range113)
(map #(t/time->value (t/date2024 % 1)))
(map #(adj/last-day-of-month-on-day-of-week % 5))
(map t/value->time)))
(take3 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
(defworking-days
(count (adj/business-days-in-range (t/time->value (t/date202461))
(t/time->value (t/date2024630)))))
; => 20 (working days in June 2024)

Recurring Event Generation

;; Every 2nd Tuesday for next 6 months
(defbi-weekly-meetings
(->> (adj/every-nth-day-of-week today 22) ; Every 2nd Tuesday
(take-while #(< % (adj/add-months today 6)))
(take12)
(map t/value->time)))
;; Quarterly board meetings (last Friday of quarter)
(defquarterly-meetings
(->> [36912] ; End of quarters
(map #(t/time->value (t/date2024 % 1)))
(map adj/end-of-month)
(map #(adj/last-day-of-month-on-day-of-week % 5))
(map t/value->time)))

Financial Calculations

;; Monthly payment dates (15th of each month)
(defpayment-dates-2024
(->> (range113)
(map #(t/time->value (t/date2024 % 15)))
(map #(if (adj/weekend? %) (adj/previous-business-day %) ; Move to Friday if weekend
%))
(map t/value->time)))
;; Quarter-end reporting dates
(defquarter-ends
(->> (range20242027)
(mapcat #(map (fn [q] (adj/end-of-quarter (t/time->value (t/date % (* q 3) 1)))) [1234]))
(map t/value->time)))

🔧 Architecture

Modular Design

timing/
├── core/ # Core time computation (timing.core) - no dependencies
├── timezones/ # IANA timezone database (timing.timezones.db)
│ ├── timing.timezones # Current rules only (~66KB)
│ └── timing.timezones.full # With historical data (~529KB)
├── holidays/ # Country-specific holidays (timing.holiday)
├── cron/ # Cron expression parser (timing.cron)
└── util/ # Utility functions (timing.util, timing.adjusters)

Design Philosophy

  1. Numeric Domain First - Computation in milliseconds, objects for display
  2. Immutable Values - All operations return new values
  3. Functional Composition - Everything chains naturally with threading macros
  4. Zero Dependencies - Pure Clojure/ClojureScript
  5. Cross-Platform - Identical behavior on JVM and JavaScript

Performance Characteristics

  • 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

🎨 Usage Patterns

Functional Pipeline Style

(defemployees [{:hire-date (t/date2023115)}
{:hire-date (t/date2023320)}
{:hire-date (t/date2023610)}])
(->> employees
(map:hire-date)
(map t/time->value) (map #(adj/add-years % 1)) ; One year anniversary
(map #(adj/next-day-of-week % 5)) ; Move to Friday
(map t/value->time) ; Back to dates
(take3))
; => (#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")

Threading Macro Style

(-> (t/date202411)
t/time->value
(adj/add-months6)
(adj/start-of-quarter)
(adj/next-business-day)
t/value->time)
; => #inst "2024-07-01T22:00:00.000-00:00"

Sequence Generation

;; Generate all Mondays in 2024
(take-while #(< % (t/time->value (t/date202511)))
(adj/every-nth-day-of-week (t/time->value (t/date202411)) 11))
;; All business days in a month
(deftoday (t/time->value (t/date2024615)))
(adj/business-days-in-range (adj/start-of-month today) (adj/end-of-month today))

🔧 Important Notes

Timezone-Aware Date Display

Due to timezone handling, dates may display with timezone offsets. This is normal and expected behavior:

(t/value->time (t/time->value (t/date2024615)))
; => #inst "2024-06-14T22:00:00.000-00:00" (with timezone offset)

📜 License

Copyright © 2018 Robert Gersak

Released under the MIT license.


Timing: A friendly approach to time computation in Clojure.

About

Time computation library with CRON scheduling capability

Topics

Resources

Stars

43 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages