(ns java-time.amount
(:require [clojure.string :as string]
[java-time.core :as jt.c]
[java-time.local :as jt.l]
[java-time.util :as jt.u]
[java-time.properties :as jt.p]
[java-time.convert :as jt.convert]
[java-time.defconversion :refer (conversion! deffactory)])
(:import [java.time Duration Period]
[java.time.temporal ChronoUnit TemporalAmount Temporal TemporalUnit]))
(defn- ^Duration d-plus [^Duration cp, ^TemporalAmount o]
(.plus cp o))
(defn- ^Duration d-minus [^Duration cp, ^TemporalAmount o]
(.minus cp o))
(extend-type Duration
jt.c/Plusable
(seq-plus [d tas]
(reduce d-plus d tas))
jt.c/Minusable
(seq-minus [d tas]
(reduce d-minus d tas))
jt.c/Multipliable
(multiply-by [d v]
(.multipliedBy d v))
jt.c/Amount
(zero? [d]
(.isZero d))
(negative? [d]
(.isNegative d))
(negate [d]
(.negated d))
(abs [d]
(.abs d))
jt.c/As
(as* [o k]
(-> (.toNanos o)
(jt.convert/convert-amount :nanos k)
:whole)))
(deffactory duration
"Creates a duration - a temporal entity representing standard days, hours,
minutes, millis, micros and nanos. The duration itself contains only seconds
and nanos as properties.
Given one argument will:
* interpret as millis if a number
* try to parse from the standard format if a string
* extract supported units from another `TemporalAmount`
* convert from a Joda Period/Duration
Given two arguments will:
* get a duration between two `Temporal`s
* get a duration of a specified unit, e.g. `(duration 100 :seconds)`"
:returns Duration
:implicit-arities [1 2]
([] (Duration/ofMillis 0)))
(defn ^java.time.Duration micros
"Duration of a specified number of microseconds."
[micros]
(Duration/ofNanos (Math/multiplyExact (long micros) 1000)))
(defmacro gen-duration-methods
[]
(conj (map (fn [[fn-name method-name]]
(let [fn-name (with-meta fn-name {:tag Duration})]
`(defn ~fn-name
~(str "Returns a `Duration` of `i` " method-name ".")
{:arglists '[[~'i]]}
[i#]
(. Duration ~(symbol (str 'of (string/capitalize (str method-name)))) (long i#)))))
[['standard-days 'days]
['hours 'hours]
['minutes 'minutes]
['millis 'millis]
['seconds 'seconds]
['nanos 'nanos]])
'do))
(gen-duration-methods)
(defmacro gen-period-methods
[]
(conj (map (fn [fn-name]
(let [fn-name (with-meta fn-name {:tag Period})]
`(defn ~fn-name
~(str "Returns a `Period` of `i` " fn-name ".")
{:arglists '[[~'i]]}
[i#]
(. Period ~(symbol (str 'of (string/capitalize (str fn-name)))) (int i#)))))
['years
'months
'days
'weeks])
'do))
(gen-period-methods)
(deffactory period
"Creates a period - a temporal entity consisting of years, months and days.
Given one argument will
* interpret as years if a number
* try to parse from the standard format if a string
* extract supported units from another `TemporalAmount`
* convert from a Joda Period
Given two arguments will
* get a period of a specified unit, e.g. `(period 10 :months)`
* get a period between two temporals by converting them to local dates
* get a period of a specified number of years and months
Given three arguments will create a year/month/day period."
:returns Period
:implicit-arities [1 2 3]
([] (Period/of 0 0 0)))
(jt.u/when-joda-time-loaded
(defn ^Period joda-period->period [^org.joda.time.Period p]
(if-not (zero? (+ (.getMillis p) (.getSeconds p) (.getMinutes p) (.getHours p)))
(throw (ex-info "Cannot convert a Joda Period containing non year/month/days to a Java-Time Period!"
{:period p}))
(Period/of (.getYears p) (.getMonths p) (+ (* 7 (.getWeeks p)) (.getDays p)))))
(defn ^Duration joda-period->duration [^org.joda.time.Period p]
(if-not (zero? (+ (.getMonths p) (.getYears p)))
(throw (ex-info "Cannot convert a Joda Period containing months/years to a Java-Time Duration!"
{:period p}))
(jt.c/plus
(duration (.getMillis p) :millis)
(duration (.getSeconds p) :seconds)
(duration (.getMinutes p) :minutes)
(duration (.getHours p) :hours)
(duration (+ (* 7 (.getWeeks p)) (.getDays p)) :days))))
(conversion! org.joda.time.Duration Duration
(fn [^org.joda.time.Duration d]
(Duration/ofMillis (.getMillis d))))
(conversion! org.joda.time.Period Duration
joda-period->duration)
(conversion! org.joda.time.Duration Period
(fn [^org.joda.time.Duration d]
(Period/ofDays (.getStandardDays ^org.joda.time.Duration d))))
(conversion! org.joda.time.Period Period
joda-period->period))
(conversion! CharSequence Duration
(fn [^CharSequence s]
(Duration/parse s)))
(conversion! CharSequence Period
(fn [^CharSequence s]
(Period/parse s)))
(conversion! Number Duration
(fn [^Number millis]
(Duration/ofMillis millis)))
(conversion! Number Period
#(Period/of (int %1) 0 0))
(conversion! [Number Number] Period
#(Period/of (int %1) (int %2) 0))
(conversion! [Number Number Number] Period
#(Period/of (int %1) (int %2) (int %3)))
(conversion! [Temporal Temporal] Duration
(fn [^Temporal a, ^Temporal b]
(Duration/between a b)))
(conversion! [Temporal Temporal] Period
(fn [^Temporal a, ^Temporal b]
(Period/between a b)))
(conversion! [Number clojure.lang.Keyword] Period
(fn [value k]
(case k
:years (years value)
:months (months value)
:days (days value))))
(conversion! [Number TemporalUnit] Duration
(fn [value ^TemporalUnit unit]
(Duration/of (long value) unit)))
(conversion! clojure.lang.Keyword TemporalUnit
jt.p/get-unit-checked)
(conversion! clojure.lang.Keyword TemporalAmount
jt.p/get-unit-checked)
(extend-type Period
jt.c/As
(as* [o k]
(if (<= (.compareTo ^ChronoUnit (jt.p/unit k) ChronoUnit/WEEKS) 0)
(if (and (zero? (.getYears o))
(zero? (.getMonths o)))
(-> (.getDays o)
(jt.convert/convert-amount :days k)
:whole)
(throw (java.time.DateTimeException. "Period contains years or months")))
(if (zero? (.getDays o))
(-> (.toTotalMonths o)
(jt.convert/convert-amount :months k)
:whole)
(throw (java.time.DateTimeException. "Period contains days"))))))