diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4075360 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +target +/classes +/checkouts +pom.xml +pom.xml.asc +*.jar +*.class +.lein-* +/.nrepl-port +.hgignore +.hg/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..13a2d46 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2015 Vadim Platonov + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..85dca7f --- /dev/null +++ b/README.md @@ -0,0 +1,380 @@ +# Clojure.Java-Time + +[![Build Status](https://travis-ci.org/dm3/clojure.java-time.png?branch=master)](https://travis-ci.org/dm3/clojure.java-time) + +An idiomatic Clojure wrapper for Java 8 Date-Time API. + +Main goals: + +* Provide a consistent API for common operations with + instants, date-times, zones and periods. +* Provide an escape hatch from Java types to clojure data structures. +* Avoid reflective calls. +* Provide an entry point into Java-Time by freeing the user from importing most + of the Java-Time classes. + +Why use Clojure.Java-Time over [clj-time](https://github.com/clj-time/clj-time) +or [Clojure.Joda-Time](https://github.com/dm3/clojure.joda-time)? + +* You don't want to have a dependency on the Joda-Time library +* You already use Java 8 +* You prefer as little Java interop code as possible + +This library employs a structured and comprehensive approach to exposing the +Java 8 Date-Time API to the Clojure world. It's very similar to +Clojure.Joda-Time in its design goals and overall feeling, so if you ever used +that you will feel at home! + +## What's different in Java Time API? + +If you already used Joda Time before you might think: "What in the world could +they do better?". After all, Joda-Time already provides a pretty comprehensive +set of tools for dealing with time-related concepts. Turns out, it's a tad more +complicated than it has to be. Also, a few concepts have faulty designs which +lead to hard to fix bugs and misuse. You can see the birds-eye view of changes +and some of the rationale on the authors' (Stephen Colebourne) blog: + +* [what's wrong with Joda-Time](http://blog.joda.org/2009/11/why-jsr-310-isn-joda-time_4941.html), +* [when you should use Java-Time](http://blog.joda.org/2014/07/threeten-backport-vs-joda-time.html) +* [what's different in Java-Time](http://blog.joda.org/2014/11/converting-from-joda-time-to-javatime.html). + +You can also take a look at a [comprehensive comparison](http://time4j.net/tutorial/appendix.html) by the +[Time4J](http://time4j.net/) authors. + +## Usage + +Add the following dependency to your `project.clj` or `build.boot`: + +```clj +[clojure.java-time "0.1.0"] +``` + +[API](http://dm3.github.io/clojure.java-time/) of the Clojure.Java-Time +consists of one namespace, namely `java-time`. For the purposes of this +guide, we will `use` the main namespace: + +```clj +(refer-clojure :exclude [range iterate format max min]) +(use 'java-time) +``` + +### Concept run-through + +Java Time API may seem daunting. Instead of a single `java.util.Date` you have +a `ZonedDateTime`, `OffsetDateTime`, `LocalDateTime`, `Instant`, and other +types. You would be well served by reading the official documentation for the +[Java Time API](https://docs.oracle.com/javase/tutorial/datetime/iso/index.html), +but we'll also do a quick run-through here. + +#### Local Dates + +`LocalDate`, `LocalTime` and `LocalDateTime` are used to represent a date, time +and date-time respectively without an offset or a timezone. The local time entities +are used to represent human-based dates/times. They are a good fit for representing +the time of various events: + +* `LocalDate` - birthday, holiday +* `LocalTime` - bus schedule, opening time of a shop +* `LocalDateTime` - start of a competition + +#### Zoned Dates + +There are two types which deal with zones: `OffsetDateTime` and +`ZonedDateTime`. They do pretty much what you would expect from their name. +You can think of the `Offset` time as a more concrete version of the `Zoned` +time. For example, the same timezone can have different offsets throughout the +year due to DST or governmental regulations. + +#### Instant + +An `Instant` is used to generate a time stamp representing machine time. It +doesn't have an offset or a time zone. You can think of it as of a number of +milliseconds since epoch (`1970-01-01T00:00:00Z`). An instant is directly +analogous to `java.util.Date`: + +```clj +user=> (java.time.Instant/now) +#object[java.time.Instant 0x1c1ac77a "2015-09-26T05:25:48.667Z"] +user=> (java.util.Date.) +#inst "2015-09-26T05:25:50.118-00:00" +``` + +Every other date entity can be converted to an instant (local ones will require +an additional zone information). + +#### Period and Duration + +Java Time Period entities are considerably simpler than the Joda-Time periods. +They are fixed containers of years, months and days. You can use them to +represent any period of time with a granularity larger or equal to a single day. +Duration, on the other hand, represents a standard duration less than or equal +to a single standard (24-hour) day. + +### An appetizer + +First, let's do a quick run through common use cases. + +What is the current date? + +```clj +(def now (local-date)) +=> #object[java.time.LocalDate "2015-09-27"] +``` + +What's the next day? + +```clj +(plus now (days 1)) +=> #object[java.time.LocalDate "2015-09-28"] +``` + +The previous day? + +```clj +(minus now (days 1)) +=> #object[java.time.LocalDate "2015-09-28"] +``` + +Three next days? + +```clj +(take 3 (iterate plus now (days 1))) +=> (#object[java.time.LocalDate "2015-09-28"] + #object[java.time.LocalDate "2015-09-29"] + #object[java.time.LocalDate "2015-09-30"]) +``` + +When is the next working day? + +```clj +(adjust now :next-working-day) +=> #object[java.time.LocalDate "2015-09-28"] +``` + +Date with some of its fields truncated: + +```clj +(truncate-to (local-date-time 2015 9 28 10 15) :days) +=> #object[java.time.LocalDateTime "2015-09-28T00:00"] +``` + +Date-time adjusted to the given hour: + +```clj +(adjust (local-date-time 2015 9 28 10 15) (local-time 6)) +=> #object[java.time.LocalDateTime "2015-09-28T06:00"] +``` + +The latest of the given dates? + +```clj +(max (local-date 2015 9 20) (local-date 2015 9 28) (local-date 2015 9 1)) +=> #object[java.time.LocalDate "2015-09-28"] +``` + +The shortest of the given durations? + +```clj +(min (duration 10 :seconds) (duration 5 :hours) (duration 3000 :millis)) +=> #object[java.time.Duration "PT3S"] +``` + +Get the year field out of the date: + +```clj +(as (local-date 2015 9 28) :year) +=> 2015 +``` + +Get multiple fields: + +```clj +(as (local-date 2015 9 28) :year :month-of-year :day-of-month) +=> (2015 9 28) +``` + +Get the duration in a different unit: + +```clj +java-time> (plus (hours 3) (minutes 2)) +#object[java.time.Duration "PT3H2M"] +java-time> (as *1 :minutes) +182 +``` + +Format a date: + +```clj +(format "MM/dd" (zoned-date-time 2015 9 28 "UTC")) +=> "09/28" +``` + +Parse a date: + +```clj +(local-date "MM/yyyy/dd" "09/2015/28") +=> #object[java.time.LocalDate "2015-09-28"] +``` + +#### Conversions + +Time entities can be converted to other time entities if the target contains +less information, e.g.: + +```clj +(zoned-date-time (offset-date-time 2015 9 28 1 +0)) +=> #object[java.time.ZonedDateTime "2015-09-28T01:00Z"] + +(instant (offset-date-time 2015 9 28 1 +0)) +=> #object[java.time.Instant "2015-09-28T01:00:00Z"] + +(offset-time (offset-date-time 2015 9 28 1 +0)) +=> #object[java.time.OffsetTime "01:00Z"] + +(local-date-time (offset-date-time 2015 9 28 1 +0)) +=> #object[java.time.LocalDateTime "2015-09-28T01:00"] + +(local-time (offset-time 1 +0)) +=> #object[java.time.LocalTime 0x3a3cd6d5 "01:00"] +``` + +Any date which can be converted to an instant, can also be converted to pre-Java +8 date types: + +```clj +(to-java-date (zoned-date-time 2015 9 28 "UTC")) +=> #inst "2015-09-27T22:00:00.000-00:00" + +(to-sql-date (zoned-date-time 2015 9 28 "UTC")) +=> #inst "2015-09-27T22:00:00.000-00:00" + +(to-sql-timestamp (zoned-date-time 2015 9 28 "UTC")) +=> #inst "2015-09-27T22:00:00.000000000-00:00" +``` + +Bonus! if you have Joda Time on the classpath, you can seamlessly convert from +Joda Time to Java Time types: + +```clj +(java-time.repl/show-path org.joda.time.DateTime java.time.OffsetTime) +=> {:cost 2.0, + :path [[# + #] + [# + #]]} + +(offset-time (org.joda.time.DateTime/now)) +=> # +``` + +#### Clocks + +Java Time introduced a concept of `Clock` - a time entity which can seed the +dates, times and zones. However, there's no built-in facility which would allow +you to influence the date-times create using default constructors ala Joda's +`DateTimeUtils/setCurrentMillisSystem`. Clojure.Java-Time tries to fix that with +the `with-clock` macro and the corresponding `with-clock-fn` function: + +```clj +(zone-id) +=> # + +(with-clock (system-clock "UTC") + (zone-id)) +=> # +``` + +Clock overrides works for all of the date-time types. + +#### Fields, Units and Properties + +Date-Time entities are composed of date fields, while Duration entities are +composed of time units. You can see all of the predefined fields and units +via the `java-time.repl` ns: + +```clj +(java-time.repl/show-fields) +=> (:aligned-day-of-week-in-month + :aligned-day-of-week-in-year + :aligned-week-of-month + :aligned-week-of-year + :am-pm-of-day + :clock-hour-of-am-pm + ...) +``` + +```clj +(java-time.repl/show-units) +=> (:centuries + :days + :decades + :eras + :forever + :half-days + ...) +``` + +You can obtain any field/unit like this: + +```clj +(field :year) +=> #object[java.time.temporal.ChronoField "Year"] + +(unit :days) +=> #object[java.time.temporal.ChronoUnit "Days"] + +(field (local-date 2015) :year) +=> #object[java.time.temporal.ChronoField "Year"] +``` + +You can obtain all of the fields/units of the temporal entity: + +```clj +(fields (local-date)) +=> {:proleptic-month #object[java.time.temporal.ChronoField ...} + +(units (duration)) +=> {:seconds #object[java.time.temporal.ChronoUnit "Seconds"], + :nanos #object[java.time.temporal.ChronoUnit "Nanos"]} +``` + +By themselves the fields and units aren't very interesting. You can get the +range of valid values for a field and a duration between two dates, but that's +about it: + +```clj +(range (field :year)) +=> #object[java.time.temporal.ValueRange "-999999999 - 999999999"] + +(range (field :day-of-month)) +=> #object[java.time.temporal.ValueRange "1 - 28/31"] + +(time-between (local-date 2015 9) (local-date 2015 9 28) :days) +=> 27 +``` + +Fields and units become interesting in conjunction with properties. Java-Time +doesn't support the concept of properties which is present in Joda-Time. There +are reasons for that which I feel are only valid in a statically-typed API like +Java's. In Clojure, properties allow expressing time entity modifications and +queries uniformly across all of the entity types. + +```clj +(def prop (property (local-date 2015 2 28) :day-of-month)) +=> #java_time.temporal.TemporalFieldProperty{...} + +(value prop) +=> 28 + +(with-min-value prop) +=> #object[java.time.LocalDate "2015-02-01"] + +(with-value prop 20) +=> #object[java.time.LocalDate "2015-02-20"] + +(with-max-value prop) +=> #object[java.time.LocalDate "2015-02-28"] + +(properties (local-date 2015 9 28)) +=> {:proleptic-month #java_time.temporal.TemporalFieldProperty{...}, ...} +``` diff --git a/dependency-test/project.clj b/dependency-test/project.clj new file mode 100644 index 0000000..e150db8 --- /dev/null +++ b/dependency-test/project.clj @@ -0,0 +1,4 @@ +(defproject test/no-deps "0.1.0-SNAPSHOT" + :description "Clojure.Java-Time with no external dependencies" + :dependencies [[clojure.java-time "0.1.0-SNAPSHOT" :exclusions [org.threeten/threeten-extra]] + [org.clojure/clojure "1.7.0"]]) diff --git a/dependency-test/test/nodeps/test.clj b/dependency-test/test/nodeps/test.clj new file mode 100644 index 0000000..c0d996e --- /dev/null +++ b/dependency-test/test/nodeps/test.clj @@ -0,0 +1,10 @@ +(ns nodeps.test + (:require [java-time :as j] + [clojure.test :refer :all])) + +(def now (j/fixed-clock (j/zoned-date-time 2015 1 1))) + +(deftest works + (is (nil? (resolve 'j/interval))) + (is (nil? (resolve 'j/am-pm))) + (is (= 180 (j/as (j/duration 3 :hours) :minutes)))) diff --git a/dev/user.clj b/dev/user.clj new file mode 100644 index 0000000..8bc23a2 --- /dev/null +++ b/dev/user.clj @@ -0,0 +1,23 @@ +(ns user + (:require [clojure.tools.namespace.repl :as repl] + [criterium.core :as crit] + [taoensso.timbre :as timbre] + [taoensso.timbre.profiling :as profiling :refer (pspy pspy* profile defnp p p*)])) + +(defn go [] + (set! *warn-on-reflection* false) + (repl/refresh-all) + (require '[java-time :as j]) + (eval `(profile :info :local-date-time (j/local-date-time 1 2 3))) + (eval `(profile :info :zoned-date-time (j/zoned-date-time 1 2 3))) + (eval `(profile :info :fail (try (j/zoned-date-time 1 2 "a") (catch Exception e# nil))))) + +(defn bench [] + (repl/refresh-all) + (require '[java-time :as j]) + (eval `(crit/bench (j/local-date-time 1 2 3)))) + +(defn print-reflection-warnings [] + (set! *warn-on-reflection* true) + (repl/refresh-all) + (set! *warn-on-reflection* false)) diff --git a/project.clj b/project.clj new file mode 100644 index 0000000..d2337fd --- /dev/null +++ b/project.clj @@ -0,0 +1,23 @@ +(defproject clojure.java-time "0.1.0-SNAPSHOT" + :description "Idiomatic Clojure wrapper for Java 8 Time API" + :url "http://github.com/dm3/clojure.java-time" + :license {:name "MIT License" + :url "http://opensource.org/licenses/MIT"} + :scm {:name "git" + :url "http://github.com/dm3/clojure.java-time"} + :dependencies [[org.threeten/threeten-extra "0.9"] + [clj-tuple "0.2.2"]] + :profiles {:dev {:dependencies [[org.clojure/clojure "1.7.0"] + [org.clojure/test.check "0.5.8"] + [criterium "0.4.2"] + [com.taoensso/timbre "4.1.4"] + [org.clojure/tools.namespace "0.2.11"] + [joda-time/joda-time "2.9"]] + :plugins [[codox "0.8.13"]] + :codox {:include [java-time]} + :source-paths ["dev"] + :global-vars {*warn-on-reflection* true}} + :1.8 {:dependencies [[org.clojure/clojure "1.8.0-RC1"]]}} + :aliases {"test-all" ["with-profile" "dev,default:dev,1.6,default:dev,1.8,default" "test"]} + :deploy-repositories [["clojars" {:url "https://clojars.org/repo" + :sign-releases false}]]) diff --git a/src/java_time.clj b/src/java_time.clj new file mode 100644 index 0000000..62e5d26 --- /dev/null +++ b/src/java_time.clj @@ -0,0 +1,72 @@ +(ns java-time + (:refer-clojure :exclude (zero? range iterate max min contains? format)) + (:require [java-time.potemkin.namespaces :as potemkin] + [java-time.util :as jt.u] + [java-time core properties temporal amount zone single-field local chrono + convert sugar seqs adjuster interval format joda clock])) + +(potemkin/import-vars + [java-time.clock + with-clock with-clock-fn] + + [java-time.core + zero? negative? negate abs max min before? after? + supports? chronology fields units properties property + as value range min-value max-value largest-min-value smallest-max-value + with-value with-min-value with-max-value with-largest-min-value with-smallest-max-value + truncate-to time-between with-zone leap? + plus minus multiply-by] + + [java-time.amount + duration period period? duration? + nanos micros millis seconds minutes hours standard-days + days weeks months years] + + [java-time.properties + unit? unit field? field] + + [java-time.temporal + value-range instant instant?] + + [java-time.local + local-date local-date-time local-time + local-date? local-date-time? local-time?] + + [java-time.single-field + year year? month month? day-of-week day-of-week? month-day month-day? + year-month year-month?] + + [java-time.zone + available-zone-ids zone-id zone-offset + offset-date-time offset-time zoned-date-time + system-clock fixed-clock offset-clock tick-clock + zoned-date-time? offset-date-time? offset-time?] + + [java-time.convert + as-map convert-amount to-java-date to-sql-date to-sql-timestamp + to-millis-from-epoch] + + [java-time.sugar + monday? tuesday? wednesday? thursday? friday? saturday? sunday? + weekend? weekday?] + + [java-time.seqs + iterate] + + [java-time.adjuster + adjust] + + [java-time.format + format formatter] + + [java-time.interval + move-start-to move-end-to move-start-by move-end-by + start end contains? overlaps? abuts? overlap gap]) + +(jt.u/when-threeten-extra + (potemkin/import-vars + [java-time.interval interval interval?] + + [java-time.single-field + am-pm am-pm? quarter quarter? day-of-month day-of-month? + day-of-year day-of-year? year-quarter year-quarter?])) diff --git a/src/java_time/adjuster.clj b/src/java_time/adjuster.clj new file mode 100644 index 0000000..1369fd4 --- /dev/null +++ b/src/java_time/adjuster.clj @@ -0,0 +1,64 @@ +(ns java-time.adjuster + (:require [java-time.util :as jt.u] + [java-time.single-field :as jt.sf]) + (:import [java.time.temporal TemporalAdjusters TemporalAdjuster])) + +(def base-adjusters {:first-day-of-month [(TemporalAdjusters/firstDayOfMonth) 0] + :last-day-of-month [(TemporalAdjusters/lastDayOfMonth) 0] + :first-day-of-next-month [(TemporalAdjusters/firstDayOfNextMonth) 0] + :first-day-of-year [(TemporalAdjusters/firstDayOfYear) 0] + :last-day-of-year [(TemporalAdjusters/lastDayOfYear) 0] + :first-day-of-next-year [(TemporalAdjusters/firstDayOfNextYear) 0] + :first-in-month [#(TemporalAdjusters/firstInMonth (jt.sf/day-of-week %)) 1] + :last-in-month [#(TemporalAdjusters/lastInMonth (jt.sf/day-of-week %)) 1] + :day-of-week-in-month [#(TemporalAdjusters/dayOfWeekInMonth + (int %1) (jt.sf/day-of-week %2)) 2] + :next-day-of-week [#(TemporalAdjusters/next (jt.sf/day-of-week %)) 1] + :next-or-same-day-of-week [#(TemporalAdjusters/nextOrSame (jt.sf/day-of-week %)) 1] + :previous-day-of-week [#(TemporalAdjusters/previous (jt.sf/day-of-week %)) 1] + :previous-or-same-day-of-week [#(TemporalAdjusters/previousOrSame (jt.sf/day-of-week %)) 1]}) + +(def extra-adjusters + (jt.u/if-threeten-extra + {:next-working-day [(org.threeten.extra.Temporals/nextWorkingDay) 0] + :previous-working-day [(org.threeten.extra.Temporals/previousWorkingDay) 0]} + {})) + +(def predefined-adjusters (merge base-adjusters extra-adjusters)) + +(defn- ^TemporalAdjuster get-adjuster [kw args] + (if-let [[adj nargs] (get predefined-adjusters kw)] + (if (zero? nargs) + adj + (if (= (count args) nargs) + (apply adj args) + (throw (java.time.DateTimeException. + (str "Adjuster: " (name kw) " cannot be created from" args "!"))))) + (throw (java.time.DateTimeException. + (str "Adjuster: " (name kw) " not found!"))))) + +(defn adjust + "Adjusts the temporal `entity` using the provided `adjuster` with optional `args`. + + The adjuster should either be a keyword which resolves to one of the + predefined adjusters (see `java-time.repl/show-adjusters`) an instance of + `TemporalAdjuster` or a function which returns another temporal entity when + applied to the given one: + + (adjust (local-date 2015 1 1) :next-working-day) + => # + + (adjust (local-date 2015 1 1) :first-in-month :monday) + => # + + (adjust (local-date 2015 1 1) plus (days 1)) + => #" + [entity adjuster & args] + (cond (instance? TemporalAdjuster adjuster) + (.adjustInto ^TemporalAdjuster adjuster ^Temporal entity) + + (fn? adjuster) + (apply adjuster entity args) + + (keyword? adjuster) + (.adjustInto (get-adjuster adjuster args) ^Temporal entity))) diff --git a/src/java_time/amount.clj b/src/java_time/amount.clj new file mode 100644 index 0000000..0d866dd --- /dev/null +++ b/src/java_time/amount.clj @@ -0,0 +1,203 @@ +(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 ^Duration micros + "Duration of a specified number of microseconds." + [micros] + (Duration/ofNanos (Math/multiplyExact (long micros) 1000))) + +(doseq [[fn-name method-name] [['standard-days 'days] + ['hours 'hours] + ['minutes 'minutes] + ['millis 'millis] + ['seconds 'seconds] + ['nanos 'nanos]]] + (let [fn-name (with-meta fn-name {:tag Duration})] + (eval + `(defn ~fn-name + ~(str "Duration of a specified number of " method-name ".") + [v#] + (. Duration ~(symbol (str 'of (string/capitalize (str method-name)))) (long v#)))))) + +(doseq [t ['years 'months 'days 'weeks]] + (let [fn-name (with-meta t {:tag Period})] + (eval + `(defn ~fn-name [v#] + (. Period ~(symbol (str 'of (string/capitalize (str t)))) (int v#)))))) + +(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 + (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")))))) diff --git a/src/java_time/chrono.clj b/src/java_time/chrono.clj new file mode 100644 index 0000000..8ce82d9 --- /dev/null +++ b/src/java_time/chrono.clj @@ -0,0 +1,69 @@ +(ns java-time.chrono + (:require [java-time.core :as jt.c]) + (:import [java.time.chrono ChronoPeriod ChronoLocalDate ChronoLocalDateTime ChronoZonedDateTime] + [java.time.temporal TemporalAmount])) + +(defn- ^ChronoPeriod cp-plus [^ChronoPeriod cp, ^TemporalAmount o] + (.plus cp o)) + +(defn- ^ChronoPeriod cp-minus [^ChronoPeriod cp, ^TemporalAmount o] + (.minus cp o)) + +(extend-type ChronoPeriod + jt.c/Plusable + (seq-plus [cp tas] + (reduce cp-plus cp tas)) + + jt.c/Minusable + (seq-minus [cp tas] + (reduce cp-minus cp tas)) + + jt.c/Multipliable + (multiply-by [cp v] + (.multipliedBy cp (int v))) + + jt.c/Amount + (zero? [cp] + (.isZero cp)) + + (negative? [cp] + (.isNegative cp)) + + (negate [cp] + (.negated cp))) + +(extend-protocol jt.c/Ordered + ChronoLocalDate + (single-after? [d o] + (.isAfter d o)) + (single-before? [d o] + (.isBefore d o)) + + ChronoLocalDateTime + (single-after? [d o] + (.isAfter d o)) + (single-before? [d o] + (.isBefore d o)) + + ChronoZonedDateTime + (single-after? [d o] + (.isAfter d o)) + (single-before? [d o] + (.isBefore d o))) + +(extend-protocol jt.c/HasChronology + ChronoPeriod + (chronology [o] + (.getChronology o)) + + ChronoLocalDate + (chronology [o] + (.getChronology o)) + + ChronoLocalDateTime + (chronology [o] + (.getChronology o)) + + ChronoZonedDateTime + (chronology [o] + (.getChronology o))) diff --git a/src/java_time/clock.clj b/src/java_time/clock.clj new file mode 100644 index 0000000..23cbcef --- /dev/null +++ b/src/java_time/clock.clj @@ -0,0 +1,30 @@ +(ns java-time.clock + (:require [java-time.core :as jt.c]) + (:import [java.time Clock Instant])) + +(def ^:dynamic ^Clock *clock* nil) + +(defn make [f] + (if *clock* + (f *clock*) + (f (Clock/systemDefaultZone)))) + +(defn with-clock-fn + "Executes the given function in the scope of the provided clock. All the + temporal entities that get created without parameters will inherit their + values from the clock." + [^Clock c f] + (binding [*clock* c] + (f))) + +(defmacro with-clock + "Executes the given `forms` in the scope of the provided `clock`. + + All the temporal entities that get created without parameters will inherit + their values from the clock: + + (with-clock (system-clock \"Europe/London\") + (zone-id)) + => #" + [c & forms] + `(with-clock-fn ~c (fn [] ~@forms))) diff --git a/src/java_time/convert.clj b/src/java_time/convert.clj new file mode 100644 index 0000000..cdf71d1 --- /dev/null +++ b/src/java_time/convert.clj @@ -0,0 +1,98 @@ +(ns java-time.convert + (:require [java-time.core :as jt.c] + [java-time.util :as jt.u] + [java-time.properties :as jt.p] + [java-time.temporal :as jt.t]) + (:import [java.time.temporal TemporalUnit ChronoUnit] + [java.time Instant] + [java.util Date] + [java.lang Math])) + +(defn as-map + "Converts a time entity to a map of property key -> value as defined by the + passed in `value-fn`. By default the actual value of the unit/field is + produced. + + (as-map (duration)) + => {:nanos 0, :seconds 0} + + (as-map (local-date 2015 1 1)) + => {:year 2015, :month-of-year 1, :day-of-month 1, ...}" + ([e] (as-map e jt.c/value)) + ([e value-fn] (jt.u/map-vals value-fn (jt.c/properties e)))) + +(defn- convert-unit [^long amount ^long from-x, ^long to-x] + ;; from and to `x` will always be positive + (if (> from-x to-x) + {:whole (Math/multiplyExact amount (quot from-x to-x)) + :remainder 0} + (let [m (quot to-x from-x)] + {:whole (quot amount m) + :remainder (rem amount m)}))) + +(defn- month-month-factor [unit] + (case unit + :months 1 + :quarter-years 3 + :years 12 + :decades 120 + :centuries 1200 + :millenia 12000)) + +(defn- precise? [^TemporalUnit unit] + (and (instance? ChronoUnit unit) + (<= (.compareTo ^ChronoUnit unit ChronoUnit/WEEKS) 0))) + +;; Implementation inspired by org.threeten.extra.Temporals/convertAmount +;; BSD Licence +(defn convert-amount + "Converts an amount from one unit to another. Returns a map of: + * `:whole` - the whole part of the conversion in the `to` unit + * `:remainder` - the remainder in the `from` unit + + Arguments may be keywords or instances of `TemporalUnit`. + + Converts between precise units - nanos up to weeks, treating days as exact + multiples of 24 hours. Also converts between imprecise units - months up to + millenia. See `ChronoUnit` and `IsoFields` for all of the supported units. + Does not convert between precise and imprecise units. + + Throws `ArithmeticException` if long overflow occurs during computation. + + (convert-amount 10000 :seconds :hours) + => {:remainder 2800 :whole 2}" + [amount from-unit to-unit] + (let [^TemporalUnit from-unit (jt.p/unit from-unit) + ^TemporalUnit to-unit (jt.p/unit to-unit)] + (if (= from-unit to-unit) + {:whole amount, :remainder 0} + (if (and (precise? from-unit) (precise? to-unit)) + (convert-unit (long amount) + (-> from-unit .getDuration .toNanos) + (-> to-unit .getDuration .toNanos)) + (convert-unit (long amount) + (month-month-factor (jt.p/unit-key from-unit)) + (month-month-factor (jt.p/unit-key to-unit))))))) + +(defn ^Date to-java-date + "Converts a date entity to a `java.util.Date`." + [o] + (if (instance? Date o) o + (Date/from (jt.t/instant o)))) + +(defn ^java.sql.Date to-sql-date + "Converts a date entity to a `java.sql.Date`." + [o] + (java.sql.Date/from (jt.t/instant o))) + +(defn ^java.sql.Timestamp to-sql-timestamp + "Converts a date entity to a `java.sql.Timestamp`." + [o] + (java.sql.Timestamp/from (jt.t/instant o))) + +(defn ^long to-millis-from-epoch + "Converts a date entity to a `long` representing the number of milliseconds + from epoch." + [o] + (if (number? o) (long o) + (.toEpochMilli (jt.t/instant o)))) diff --git a/src/java_time/core.clj b/src/java_time/core.clj new file mode 100644 index 0000000..d0e4d61 --- /dev/null +++ b/src/java_time/core.clj @@ -0,0 +1,248 @@ +(ns java-time.core + (:refer-clojure :exclude (zero? range max min)) + (:import [java.time.temporal ValueRange] + [java.time.chrono Chronology])) + +(defprotocol Amount + (zero? [a] + "True if the amount is zero") + (negative? [a] + "True if the amount is negative") + (negate [a] + "Negates a temporal amount: + + (negate (negate x)) == x") + (abs [a] + "Returns the absolute value of a temporal amount: + + (abs (negate x)) == (abs x)")) + +(defprotocol Supporting + (supports? [o p] + "True if the `o` entity supports the `p` property")) + +(defprotocol HasChronology + (^Chronology chronology [o] + "The `Chronology` of the entity")) + +(defprotocol HasFields + (fields [o] + "Fields present in this temporal entity") + (field* [o k] + "Internal use")) + +(defprotocol HasUnits + (units [o] + "Units present in this temporal entity.") + (unit* [o k] + "Internal use")) + +(defprotocol HasProperties + (properties [o] + "Map of properties present in this temporal entity") + (property [o k] + "Property of this temporal entity under key `k`")) + +(defprotocol As + (as* [o k] + "Value of property/unit identified by key/object `k` of the temporal + entity `o`")) + +(defn as + "Values of property/unit identified by keys/objects `ks` of the temporal + entity `o`, e.g. + + (as (duration 1 :hour) :minutes) + => 60 + + (as (local-date 2015 9) :year :month-of-year) + => [2015 9]" + ([o k] + (as* o k)) + ([o k1 k2] + [(as* o k1) (as* o k2)]) + ([o k1 k2 & ks] + (concat (as o k1 k2) (mapv #(as* o %) ks)))) + +(defprotocol ReadableProperty + (value [p] + "Value of the property")) + +(defprotocol ReadableRangeProperty + (range [p] + "Range of values for this property") + (min-value [p] + "Minimum value of this property") + (largest-min-value [p] + "Largest minimum value of this property") + (smallest-max-value [p] + "Smallest maximum value of this property, e.g. 28th of February for months") + (max-value [p] + "Maximum value of this property, e.g. 29th of February for months")) + +(defprotocol WritableProperty + (with-value [p v] + "Underlying temporal entity with the value of this property set to `v`")) + +(defprotocol WritableRangeProperty + (with-min-value [p] + "Underlying temporal entity with the value set to the minimum available for + this property") + (with-largest-min-value [p] + "Underlying temporal entity with the value set to the largest minimum + available for this property") + (with-smallest-max-value [p] + "Underlying temporal entity with the value set to the smallest maximum + available for this property") + (with-max-value [p] + "Underlying temporal entity with the value set to the maximum + available for this property")) + +(defprotocol KnowsTimeBetween + (time-between [o e u] + "Time between temporal entities `o` and `e` in unit `u`. + + (j/time-between (j/local-date 2015) (j/local-date 2016) :days) + => 365 + + (j/time-between :days (j/local-date 2015) (j/local-date 2016)) + => 365")) + +(defprotocol KnowsIfLeap + (leap? [o] + "True if the year of this entity is a leap year.")) + +(defprotocol Truncatable + (truncate-to [o u] + "Truncates this entity to the specified time unit. Only works for units that + divide into the length of standard day without remainder (up to `:days`).")) + +(defprotocol HasZone + (with-zone [o z] + "Returns this temporal entity with the specified `ZoneId`")) + +(defprotocol Plusable + "Internal" + (seq-plus [o os])) + +(defprotocol Minusable + "Internal" + (seq-minus [o os])) + +(defprotocol Multipliable + (multiply-by [o v] + "Entity `o` mutlitplied by the value `v`")) + +(defprotocol Ordered + (single-before? [a b] + "Internal use") + (single-after? [a b] + "Internal use")) + +(defn max + "Latest/longest of the given time entities. Entities should be of the same + type" + [o & os] + (last (sort (cons o os)))) + +(defn min + "Earliest/shortest of the given time entities. Entities should be of the same + type" + [o & os] + (first (sort (cons o os)))) + +(defn before? + "Returns non-nil if time entities are ordered from the earliest to the latest + (same semantics as `<`): + + (before? (local-date 2009) (local-date 2010) (local-date 2011)) + => truthy... + + (before? (interval (instant 10000) (instant 1000000)) + (instant 99999999)) + => truthy..." + ([x] true) + ([x y] (single-before? x y)) + ([x y & more] + (if (before? x y) + (if (next more) + (recur y (first more) (next more)) + (before? y (first more))) + false))) + +(defn after? + "Returns non-nil if time entities are ordered from the earliest to the latest + (same semantics as `>`): + + (after? (local-date 2011) (local-date 2010) (local-date 2009)) + => truthy... + + (after? (instant 99999999) + (interval (instant 10000) (instant 1000000))) + => truthy..." + ([x] true) + ([x y] (single-after? x y)) + ([x y & more] + (if (after? x y) + (if (next more) + (recur y (first more) (next more)) + (after? y (first more))) + false))) + +(defn plus + "Adds all of the `os` to the time entity `o` + + (j/plus (j/local-date 2015) (j/years 1)) + => " + [o & os] + (seq-plus o os)) + +(defn minus + "Subtracts all of the `os` from the time entity `o` + + (j/minus (j/local-date 2015) (j/years 1)) + => " + [o & os] + (if (seq os) + (seq-minus o os) + (negate o))) + +;;;;; Clojure types + +(extend-type Number + ReadableProperty + (value [n] n) + + WritableProperty + (with-value [n v] v) + + Plusable + (seq-plus [n xs] + (apply + n xs)) + + Minusable + (seq-minus [n xs] + (apply - n xs)) + + Multipliable + (multiply-by [n v] + (* n (value v))) + + Amount + (zero? [n] (clojure.core/zero? n)) + (negative? [n] (neg? n)) + (negate [n] (- n)) + (abs [n] (Math/abs (long n)))) + +(extend-type nil + ReadableProperty + (value [_] nil) + + WritableProperty + (with-value [_ v] v)) + +(def readable-range-property-fns + {:min-value (fn [p] (.getMinimum ^ValueRange (range p))) + :largest-min-value (fn [p] (.getLargestMinimum ^ValueRange (range p))) + :smallest-max-value (fn [p] (.getSmallestMaximum ^ValueRange (range p))) + :max-value (fn [p] (.getMaximum ^ValueRange (range p)))}) diff --git a/src/java_time/defconversion.clj b/src/java_time/defconversion.clj new file mode 100644 index 0000000..f198e1f --- /dev/null +++ b/src/java_time/defconversion.clj @@ -0,0 +1,99 @@ +(ns java-time.defconversion + (:refer-clojure :exclude (vector)) + (:require [java-time.graph :as g] + [clj-tuple :refer (vector)])) + +(def graph (atom (g/conversion-graph))) + +(defn- check-arity [t vs] + (when-not (= (g/arity t) (count vs)) + (throw (ex-info (format "Arity of %s doesn't match the values %s!" t vs) + {:types t, :values vs}))) + vs) + +(defn- to-seq [in] + (if (sequential? in) + in + (vector in))) + +(defn- wrap-validation [from to f] + (fn [vs] + (let [result (apply f (check-arity from vs))] + (check-arity to (to-seq result))))) + +(defn- combinations [xs f cost] + (let [idxs (g/continuous-combinations (count xs))] + (for [combo idxs] + (vector (fn [& vs] + (let [res (to-seq (apply f vs))] + (subvec res (first combo) (inc (last combo))))) + (g/types (subvec xs (first combo) (inc (last combo)))) + (if (= (count idxs) (count xs)) + cost + ;; TODO: mark as lossy conversion + ;; currently we just incur a 0.5*number of types dropped penalty + (+ cost (* 0.5 (- (count xs) (count combo))))))))) + +(defn conversion! + ([from to f] (conversion! from to f 1)) + ([from-type-vec to-type-vec f cost] + (let [from (g/types (to-seq from-type-vec)) + tos (combinations (to-seq to-type-vec) f cost)] + (doseq [[f to cost] tos] + (swap! graph + (fn [g] + (if-let [existing (g/get-conversion g from to)] + (throw (ex-info (format "Conversion %s -> %s already exists: %s!" from to existing) + {:from from, :to to, :existing existing})) + (let [f (wrap-validation from to f)] + (g/assoc-conversion g from to f cost))))))))) + +(defn types-of [xs] + (g/types (map type xs))) + +(defn- call-conversion [nm tp args] + `(if-let [[path# fn#] (g/conversion-fn + @graph + (types-of ~args) + (g/types ~(to-seq tp)))] + (if-let [result# + (try (fn# ~args) + (catch Exception e# + (throw + (ex-info "Conversion failed" + {:path (:path path#), :arguments ~args, :to ~tp} e#))))] + (if (instance? clojure.lang.ISeq ~tp) + result# + (first result#)) + (throw (ex-info + (format "Conversion from %s to %s returned nil!" + ~args ~tp ~(str nm)) + {:arguments ~args, :to ~tp, :constructor ~nm}))) + (throw (ex-info (format "Could not convert %s to %s!" ~args ~tp ~(str nm)) + {:arguments ~args, :to ~tp, :constructor ~nm})))) + +(defn- gen-implicit-arities [nm tp arities] + (for [arity arities] + (let [args (mapv #(gensym (str "arg_" (inc %) "_")) (range arity))] + `([~@args] + ~(call-conversion nm tp args))))) + +(defn get-path [from to] + (let [[p _] (g/conversion-fn @graph + (g/types (to-seq from)) + (g/types (to-seq to)))] + (select-keys p [:path :cost]))) + +(defmacro deffactory [nm docstring _ tp _ implicit-arities & fn-bodies] + (let [fn-name (with-meta nm {:tag tp}) + explain-fn-name (symbol (str "path-to-" nm)) + predicate-name (symbol (str nm "?"))] + `(do (defn ~fn-name ~docstring + ~@(concat + fn-bodies + (gen-implicit-arities nm tp implicit-arities))) + + (defn ~predicate-name + ~(str "True if an instance of " tp ".") + [v#] + (instance? ~tp v#))))) diff --git a/src/java_time/format.clj b/src/java_time/format.clj new file mode 100644 index 0000000..69c53d1 --- /dev/null +++ b/src/java_time/format.clj @@ -0,0 +1,55 @@ +(ns java-time.format + (:refer-clojure :exclude (format)) + (:require [clojure.string :as string] + [java-time.core :as jt.c] + [java-time.util :as jt.u]) + (:import [java.time.temporal TemporalAccessor] + [java.time.format DateTimeFormatter DateTimeFormatterBuilder ResolverStyle])) + +(def predefined-formatters + (->> (jt.u/get-static-fields-of-type DateTimeFormatter DateTimeFormatter) + (jt.u/map-kv + (fn [^String n fmt] + [(string/lower-case (.replace n \_ \-)) fmt])))) + +(defn- get-resolver-style [s] + (if (instance? ResolverStyle s) s + (case s + :strict ResolverStyle/STRICT + :smart ResolverStyle/SMART + :lenient ResolverStyle/LENIENT))) + +(defn ^DateTimeFormatter formatter + "Constructs a DateTimeFormatter out of a + + * format string - \"YYYY/mm/DD\", \"YYY HH:MM\", etc. + * formatter name - :date, :time-no-millis, etc. + + Accepts a map of options as an optional second argument: + + * `resolver-style` - either `:strict`, `:smart `or `:lenient`" + ([fmt] + (formatter fmt {})) + ([fmt {:keys [resolver-style]}] + (let [^DateTimeFormatter fmt + (cond (instance? DateTimeFormatter fmt) fmt + (string? fmt) (DateTimeFormatter/ofPattern fmt) + :else (get predefined-formatters (name fmt))) + fmt (if resolver-style + (.withResolverStyle fmt (get-resolver-style resolver-style)) + fmt)] + fmt))) + +(defn format + "Formats the given time entity as a string. + + Accepts something that can be converted to a `DateTimeFormatter` as a first + argument. Given one argument uses the default format." + ([o] (str o)) + ([fmt o] + (.format (formatter fmt) o))) + +(defn ^TemporalAccessor parse + ([fmt o] (parse fmt o {})) + ([fmt o opts] + (.parse (formatter fmt opts) o))) diff --git a/src/java_time/graph.clj b/src/java_time/graph.clj new file mode 100644 index 0000000..388a578 --- /dev/null +++ b/src/java_time/graph.clj @@ -0,0 +1,317 @@ +(ns java-time.graph + (:refer-clojure :exclude (vector)) + (:require [clojure.set :as sets] + [clojure.string :as string] + ;; ~10-15% faster with clj-tuple + [clj-tuple :refer (vector)] + [java-time.potemkin.util :as u] + [java-time.util :as jt.u]) + (:import [java.util PriorityQueue Queue])) + +;; Concept heavily inspired by Zach Tellman's ByteStreams +;; https://github.com/ztellman/byte-streams/blob/master/src/byte_streams/graph.clj + +(deftype Conversion [f ^double cost] + Object + (equals [_ x] + (and + (instance? Conversion x) + (identical? f (.f ^Conversion x)) + (== cost (.cost ^Conversion x)))) + (hashCode [_] + (bit-xor (System/identityHashCode f) (unchecked-int cost))) + (toString [_] + (str "Cost:" cost))) + +(deftype Types [types ^int arity] + Object + (equals [_ o] + (and (instance? Types o) + (and (= arity (.arity ^Types o)) + (loop [idx 0] + (if (= (nth types idx) (nth (.types ^Types o) idx)) + (if (> arity (inc idx)) + (recur (inc idx)) + true) + false))))) + (hashCode [o] + (bit-xor (hash types) arity)) + (toString [_] + (pr-str types))) + +(defn arity [^Types t] + (.arity t)) + +(defn types->str [^Types t] + (.toString t)) + +(def max-arity 3) +(def max-cost 8) +(def max-path-length 4) +(def max-extent 2) + +(defn types [ts] + (let [cnt (count ts)] + (when (> cnt max-arity) + (throw (ex-info (format "Maximum arity supported by conversion graph is %s!" max-arity) + {:types ts}))) + (Types. (vec ts) cnt))) + +(defn- assignable-type? [a b] + (or (= a b) (.isAssignableFrom ^Class b a))) + +(def assignable? + ^{:doc "True if `a` is assignable to `b`, e.g. Integer is assignable to Number."} + (u/fast-memoize + (fn [^Types a ^Types b] + (or (= a b) + (and (= (.arity a) (.arity b)) + (let [ta (.types a), tb (.types b)] + (loop [idx 0] + (when (assignable-type? (nth ta idx) (nth tb idx)) + (if (> (.arity a) (inc idx)) + (recur (inc idx)) + true))))))))) + +(defprotocol IConversionGraph + (get-conversion [_ src dst]) + (assoc-conversion [_ src dst f cost]) + (equivalent-targets [_ dst]) + (possible-sources [_]) + (possible-conversions [_ src])) + +(defn- expand [[a b]] + (when-not (empty? b) + (cons + [(conj a (first b)) (rest b)] + (expand [a (rest b)])))) + +(defn- combinations [n s] + (letfn [(combos [n s] + (if (zero? n) + (list (vector (vector) s)) + (mapcat expand (combos (dec n) s))))] + (map first (combos n s)))) + +(def continuous-combinations + (u/fast-memoize + (fn [n] + (let [rng (range n)] + (->> (map inc rng) + (map #(combinations % rng)) + (apply concat) + (filterv #(apply = 1 (map - (rest %) %)))))))) + +(defn- as-source [types-so-far t [dst c]] + (vector + (vector (types (conj types-so-far t)) dst) + c)) + +(defn- search-for-possible-sources + [vresult m types-so-far k more-arity-steps] + (u/doit [[t r] m] + (when (assignable-type? k t) + (if-not more-arity-steps + (vswap! vresult concat (mapv #(as-source types-so-far t %) r)) + (search-for-possible-sources vresult r + (conj types-so-far t) + (first more-arity-steps) + (next more-arity-steps)))))) + +(defn- collect-targets [v] + (reduce + (fn [r [k v]] + (if (map? v) + (concat r (collect-targets v)) + (concat r v))) + (vector) v)) + +(defn- add-conversion [m ^Types src dst conversion] + (let [add #(update % (peek (.types src)) + (fnil conj (vector)) + (vector dst conversion))] + (if (> (.arity src) 1) + (update-in m (pop (.types src)) add) + (add m)))) + +(deftype ConversionGraph [m-by-arity srcs] + IConversionGraph + (get-conversion [_ src dst] + (let [m (get m-by-arity (.arity ^Types src))] + (->> (get-in m (.types ^Types src)) + (some #(= dst (first %)))))) + (assoc-conversion [_ src dst f cost] + (ConversionGraph. + (update m-by-arity (.arity ^Types src) + add-conversion src dst (Conversion. f cost)) + (conj srcs src))) + (possible-sources [_] srcs) + (equivalent-targets [_ dst] + (->> (vals m-by-arity) + (mapcat collect-targets) + (map first) + (filter #(assignable? % dst)) + (set))) + (possible-conversions [_ src] + (let [result (volatile! (vector))] + (search-for-possible-sources + result + (get m-by-arity (.arity ^Types src)) + (vector) + (first (.types ^Types src)) + (next (.types ^Types src))) + @result))) + +(defn conversion-graph [] + (ConversionGraph. + (zipmap (map inc (range max-arity)) (repeat {})) #{})) + +(defrecord ConversionPath [path fns visited? cost] + Comparable + (compareTo [_ x] + (let [cmp (compare cost (.cost ^ConversionPath x))] + (if (zero? cmp) + (compare (count path) (count (.path ^ConversionPath x))) + cmp))) + Object + (toString [_] + (str path cost))) + +(defn- conj-path [^ConversionPath p src dst ^Conversion c] + (ConversionPath. + (conj (.path p) (vector src dst)) + (conj (.fns p) (.f c)) + (conj (.visited? p) dst) + (+ (.cost p) (.cost c)))) + +(def graph-conversion-path + (fn [g src dst] + (let [path (ConversionPath. (vector) (vector) #{src} 0)] + (if (assignable? src dst) + path + (let [q (doto (PriorityQueue.) (.add path)) + dsts (equivalent-targets g dst)] + (loop [] + (when-let [^ConversionPath p (.poll q)] + (let [curr (or (-> p .path last second) src)] + (if (some #(assignable? curr %) dsts) + p + (do + (u/doit [[[src dst] c] (possible-conversions g curr)] + (when (and (> max-path-length (count (.path p))) + (not ((.visited? p) dst))) + (.add q (conj-path p src dst c)))) + (recur))))))))))) + +(defn- replace-range [v replacement idxs] + (concat (subvec v 0 (first idxs)) + replacement + (subvec v (inc (last idxs)) (count v)))) + +(defn- index-conversions [^Types src idxs [[_ ^Types replacement] ^Conversion conv]] + (vector src (types (replace-range (.types src) (.types replacement) idxs)) + (fn [vs] + (let [vs (vec vs)] + (replace-range vs + ((.f conv) (subvec vs (first idxs) (inc (last idxs)))) + idxs))) + (.cost conv))) + +(defn- sub-conversions + "Given an `src` types, generate all of the conversions from these types that + are possible to achieve in one step in the provided conversion graph `g`. + + For example: + + g = [[String -> Integer] [Integer -> Keyword] [[String Integer] -> String] + src = [String Integer] + + result = [[src -> [String Keyword]] + [src -> [Integer Integer]] + [src -> [Integer Keyword]] + [src -> String]" + [g ^Types src] + (if (> (.arity src) max-arity) + (vector) + (->> (continuous-combinations (.arity src)) + (mapcat + (fn [idxs] + (let [^Types input (types (subvec (.types src) (first idxs) (inc (last idxs))))] + (->> (possible-conversions g input) + (filter (fn [[[_ ^Types replacement] _]] + (>= max-arity (+ (.arity replacement) + (- (.arity src) (.arity input)))))) + (map #(index-conversions src idxs %))))))))) + +(defn- with-conversions [g convs] + (loop [g g, new-conversions (vector) + [src dst f cost :as con] (first convs) + convs (next convs)] + (if con + (if (get-conversion g src dst) + (recur g new-conversions + (first convs) (next convs)) + (recur (assoc-conversion g src dst f cost) + (conj new-conversions con) + (first convs) (next convs))) + [new-conversions g]))) + +;; Heuristic: +;; we want to skip the branches that contain the destination types as their part. +;; In our conversion world it's very unlikely that a value will be reduced to +;; the value of the same type. +(def contains-types? + "True if `a` contains `b` as its part." + (fn [^Types a, ^Types b] + (and (not= (.arity a) (.arity b)) + (let [ta (.types a), tb (.types b)] + (loop [idx 0] + (when (>= (.arity a) (+ idx (.arity b))) + (if (= tb (subvec ta idx (+ idx (.arity b)))) + true + (recur (inc idx))))))))) + +;; if a graph's sources do not contain all of the types present in the +;; requested source and the destination doesn't contain them either we conclude +;; that it's impossible to convert the source to the destination. +(defn- has-source-type? [g ^Types src, ^Types dst] + (let [src-types (map (comp types vector) (.types src)) + contains-src-types? (fn [s] (some #(or (assignable? % s) + (contains-types? s %)) src-types))] + (or (contains-src-types? dst) + (->> (possible-sources g) + (some contains-src-types?))))) + +(defn- expand-frontier [g ^Types src max-extent] + (loop [g g, q (-> (clojure.lang.PersistentQueue/EMPTY) (conj [src 0]))] + (if-let [[next-src step] (peek q)] + (if (> step max-extent) + g + (let [more-conversions (sub-conversions g next-src) + [new-conversions g'] (with-conversions g more-conversions) + accepted-conversions (filter (fn [[conv-src _ _ cost]] + (>= max-cost cost)) new-conversions)] + (recur g' (reduce (fn [q [_ dst _ _]] (conj q (vector dst (inc step)))) + (pop q) accepted-conversions)))) + g))) + +(def conversion-path + (u/fast-memoize + (fn [^ConversionGraph g, ^Types src, ^Types dst] + (when (has-source-type? g src dst) + (let [g' (expand-frontier g src max-extent)] + (graph-conversion-path g' src dst)))))) + +(defn- convert-via [path] + (condp = (count (:path path)) + 0 (vector path (fn [x] x)) + 1 (vector path (->> path :fns first)) + (let [fns (->> path :fns (apply vector))] + (vector path (fn [v] (reduce (fn [v f] (f v)) v fns)))))) + +(defn conversion-fn + "Create a function which will convert between the `src` and the `dst` + `Types`." + [g src dst] + (when-let [path (conversion-path g src dst)] + (convert-via path))) diff --git a/src/java_time/interval.clj b/src/java_time/interval.clj new file mode 100644 index 0000000..c3612c3 --- /dev/null +++ b/src/java_time/interval.clj @@ -0,0 +1,148 @@ +(ns java-time.interval + (:refer-clojure :exclude [contains?]) + (:require [clojure.string :as string] + [java-time.util :as jt.u] + [java-time.core :as jt.c] + [java-time.temporal :as jt.t] + [java-time.amount :as jt.a]) + (:import [java.time Instant Duration])) + +(defprotocol ^:private AnyInterval + (seq-move-start-by [i os] + "Use `move-start-by` with vararags") + (seq-move-end-by [i os] + "Use `move-end-by` with vararags") + (move-start-to [i new-start] + "Moves the start instant of the interval to the given instant (or something + convertible to an instant): + + (move-start-to (interval 0 10000) (instant 5000)) + => # + + Fails if the new start instant falls after the end instant: + + (move-start-to (interval 0 10000) (millis 15000)) + => DateTimeException...") + (move-end-to [i new-end] + "Moves the end of the interval to the given instant (or something + convertible to an instant): + + (move-end-to (interval 0 10000) (instant 15000)) + => # + + Fails if the new end instant falls before the start instant: + + (move-end-to (interval 0 10000) (millis -1)) + => DateTimeException...") + (start [i] "Gets the start instant of the interval") + (end [i] "Gets the end instant of the interval") + (contains? [i o] "True if the interval contains the given instant or interval") + (overlaps? [i oi] "True if this interval overlaps the other one") + (abuts? [i oi] "True if this interval abut with the other one") + (overlap [i oi] "Gets the overlap between this interval and the other one or `nil`") + (gap [i oi] "Gets the gap between this interval and the other one or `nil`")) + +;;;;;;;;;;;;; ReadableInterval + +(jt.u/when-threeten-extra + (import [org.threeten.extra Interval]) + (defn interval? + "True if `Interval`" + [o] (instance? Interval o)) + + (defn ^Interval interval + "Constructs an interval out of a string, start and end instants or a start + + duration: + + (j/interval \"2010-01-01T00:00:00Z/2013-01-01T00:00:00Z\") + => # + + (j/interval (j/instant 100000) (j/instant 1000000)) + => # + + (j/interval (j/instant 100000) (j/duration 15 :minutes)) + => #" + ([^String o] (Interval/parse o)) + ([a b] + (cond (and (jt.t/instant? a) (jt.t/instant? b)) + (Interval/of ^Instant a ^Instant b) + + (jt.a/duration? b) + (Interval/of (jt.t/instant a) ^Duration b) + + :else (Interval/of (jt.t/instant a) (jt.t/instant b))))) + + (defn- with-start [^Interval i ^Instant s] + (.withStart i s)) + + (defn- with-end [^Interval i ^Instant e] + (.withEnd i e)) + + (extend-type Interval + AnyInterval + (seq-move-start-by [i os] + (let [^Instant s (jt.c/seq-plus (.getStart i) os)] + (with-start i s))) + (seq-move-end-by [i os] + (let [^Instant e (jt.c/seq-plus (.getEnd i) os)] + (with-end i e))) + (move-start-to [i new-start] + (with-start i (jt.t/instant new-start))) + (move-end-to [i new-end] + (with-end i (jt.t/instant new-end))) + (start [i] (.getStart i)) + (end [i] (.getEnd i)) + (contains? [i o] (if (interval? o) + (.encloses i ^Interval o) + (.contains i (jt.t/instant o)))) + (overlaps? [i oi] (.overlaps i oi)) + (abuts? [i oi] (.abuts i oi)) + + (overlap [self ^Interval i] + (when (overlaps? self i) + (interval (jt.c/max (start self) (start i)) + (jt.c/min (end self) (end i))))) + (gap [self ^Interval i] + (cond (.isAfter (.getStart self) (.getEnd i)) + (interval (.getEnd i) (.getStart self)) + + (.isBefore (.getEnd self) (.getStart i)) + (interval (.getEnd self) (.getStart i)))) + + jt.c/Ordered + (single-before? [i o] (if (jt.t/instant? o) + (.isBefore (.getEnd i) o) + (.isBefore (.getEnd i) (.getStart ^Interval o)))) + (single-after? [i o] (if (jt.t/instant? o) + (.isAfter (.getStart i) o) + (.isAfter (.getStart i) (.getEnd ^Interval o)))) + + jt.c/As + (as* [o k] + (jt.c/as (.toDuration o) k)))) + +(defn move-start-by + "Moves the start instant of the interval by the sum of given + periods/durations/numbers of milliseconds: + + (move-start-by (interval 0 10000) (millis 1000) (seconds 1)) + => # + + Fails if the new start instant falls after the end instant. + + (move-start-by (interval 0 10000) (millis 11000)) + ; => DateTimeException..." + [i & os] (seq-move-start-by i os)) + +(defn move-end-by + "Moves the end instant of the interval by the sum of given + periods/durations/numbers of milliseconds. + + (move-start-by (interval 0 10000) (millis 1000) (seconds 1)) + => # + + Fails if the new end instant falls before the start instant. + + (move-end-by (interval 0 10000) (millis -11000)) + => DateTimeException..." + [i & os] (seq-move-end-by i os)) diff --git a/src/java_time/joda.clj b/src/java_time/joda.clj new file mode 100644 index 0000000..055b21a --- /dev/null +++ b/src/java_time/joda.clj @@ -0,0 +1,60 @@ +(ns java-time.joda + (:require [java-time.util :as jt.u] + [java-time.local :as jt.l :refer (local-date local-time local-date-time)] + [java-time.zone :as jt.z :refer (zoned-date-time offset-date-time offset-time)] + [java-time.temporal :as jt.t :refer (instant)] + [java-time.defconversion :refer (conversion! deffactory)]) + (:import [java.time LocalDate LocalTime LocalDateTime ZoneId Instant + ZonedDateTime OffsetDateTime OffsetTime])) + +(jt.u/when-joda + (defn joda-fields [o fields] + (mapv (fn [[^org.joda.time.DateTimeFieldType f mul]] + (* mul (if (instance? org.joda.time.ReadableInstant o) + (.get ^org.joda.time.ReadableInstant o f) + (.get ^org.joda.time.ReadablePartial o f)))) fields)) + + (defn from-joda-fields [o fields constructor] + (apply constructor (joda-fields o fields))) + + (def joda-date-fields + [[(org.joda.time.DateTimeFieldType/year) 1] + [(org.joda.time.DateTimeFieldType/monthOfYear) 1] + [(org.joda.time.DateTimeFieldType/dayOfMonth) 1]]) + + (def joda-time-fields + [[(org.joda.time.DateTimeFieldType/hourOfDay) 1] + [(org.joda.time.DateTimeFieldType/minuteOfHour) 1] + [(org.joda.time.DateTimeFieldType/secondOfMinute) 1] + [(org.joda.time.DateTimeFieldType/millisOfSecond) (* 1000 1000)]]) + + (def joda-date-time-fields + (concat joda-date-fields joda-time-fields)) + + ;; local + (conversion! org.joda.time.ReadablePartial LocalDateTime + (fn [^org.joda.time.ReadablePartial i] + (from-joda-fields i joda-date-time-fields local-date-time))) + + (conversion! org.joda.time.ReadablePartial LocalDate + (fn [^org.joda.time.ReadablePartial i] + (from-joda-fields i joda-date-fields local-date))) + + (conversion! org.joda.time.ReadablePartial LocalTime + (fn [^org.joda.time.ReadablePartial i] + (from-joda-fields i joda-time-fields local-time))) + + (defn ^ZoneId from-joda-zone [^org.joda.time.DateTimeZone dtz] + (ZoneId/of (.getID dtz))) + + (conversion! org.joda.time.DateTime [Instant ZoneId] + (fn [^org.joda.time.ReadableInstant i] + [(instant (.getMillis i)) + (from-joda-zone (.getZone i))])) + + (conversion! org.joda.time.Instant Instant + (fn [^org.joda.time.Instant i] + (instant (.getMillis i)))) + + (conversion! org.joda.time.DateTimeZone java.time.ZoneId + from-joda-zone)) diff --git a/src/java_time/local.clj b/src/java_time/local.clj new file mode 100644 index 0000000..de5928b --- /dev/null +++ b/src/java_time/local.clj @@ -0,0 +1,181 @@ +(ns java-time.local + (:require [java-time.core :as jt.c :refer (value)] + [java-time.properties :as jt.p] + [java-time.temporal :as jt.t] + [java-time.format :as jt.f] + [java-time.clock :as jt.clock] + [java-time.defconversion :refer (conversion! deffactory)]) + (:import [java.time ZoneId Clock LocalDate LocalTime LocalDateTime Instant + ZonedDateTime OffsetDateTime OffsetTime] + [java.time.temporal TemporalAccessor])) + +(deffactory local-date + "Creates a `LocalDate`. The following arguments are supported: + + * no arguments - current local-date + * one argument + + clock + + another temporal entity + + string representation + + year + * two arguments + + formatter (format) and a string + + an instant and a zone id + + another temporal entity and an offset (preserves local time) + + year and month + * three arguments + + year, month and date" + :returns LocalDate + :implicit-arities [1 2 3] + ([] (jt.clock/make (fn [^Clock c] (LocalDate/now c))))) + +(deffactory local-time + "Creates a `LocalTime`. The following arguments are supported: + + * no arguments - current local time + * one argument + + clock + + another temporal entity + + string representation + + hours + * two arguments + + formatter (format) and a string + + an instant and a zone id + + hours and minutes + * three/four arguments - hour, minute, second, nanos" + :returns LocalTime + :implicit-arities [1 2 3] + ([] (jt.clock/make (fn [^Clock c] (LocalTime/now c)))) + ([h m s nn] + (LocalTime/of (int h) (int m) (int s) (int nn)))) + +(deffactory local-date-time + "Creates a `LocalDateTime`. The following arguments are supported: + + * no arguments - current local date-time + * one argument + + clock + + another temporal entity + + string representation + + year + * two arguments + + local date and local time + + an instant and a zone id + + formatter (format) and a string + + year and month + + three and more arguments - year/month/day/..." + :returns LocalDateTime + :implicit-arities [1 2 3] + ([] (jt.clock/make (fn [^Clock c] (LocalDateTime/now c)))) + ([y m d h] + (local-date-time y m d h 0)) + ([y m d h mm] + (local-date-time y m d h mm 0)) + ([y m d h mm ss] + (local-date-time y m d h mm ss 0)) + ([y m d h mm ss n] + (LocalDateTime/of (int (value y)) (int (value m)) (int (value d)) + (int h) (int mm) (int ss) (int n)))) + +(extend-type LocalTime + jt.c/Truncatable + (truncate-to [o u] + (.truncatedTo o (jt.p/get-unit-checked u))) + + jt.c/Ordered + (single-after? [t o] + (.isAfter t o)) + (single-before? [t o] + (.isBefore t o))) + +(extend-type LocalDateTime + jt.c/Truncatable + (truncate-to [o u] + (.truncatedTo o (jt.p/get-unit-checked u)))) + +(conversion! Clock LocalDate + (fn [^Clock c] + (LocalDate/now c))) + +(conversion! Clock LocalTime + (fn [^Clock c] + (LocalTime/now c))) + +(conversion! Clock LocalDateTime + (fn [^Clock c] + (LocalDateTime/now c))) + +(conversion! CharSequence LocalDate + (fn [^CharSequence s] + (LocalDate/parse s))) + +(conversion! CharSequence LocalTime + (fn [^CharSequence s] + (LocalTime/parse s))) + +(conversion! CharSequence LocalDateTime + (fn [^CharSequence s] + (LocalDateTime/parse s))) + +(conversion! LocalTime LocalDateTime + (fn [^LocalTime lt] + (.atDate lt (local-date))) + 2) + +(conversion! LocalDate LocalDateTime + (fn [^LocalDate ld] + (.atTime ld (local-time))) + 2) + +(conversion! [java.time.format.DateTimeFormatter CharSequence] LocalDate + #(LocalDate/from (jt.f/parse %1 %2))) + +(conversion! [java.time.format.DateTimeFormatter CharSequence] LocalTime + #(LocalTime/from (jt.f/parse %1 %2))) + +(conversion! [java.time.format.DateTimeFormatter CharSequence] LocalDateTime + #(LocalDateTime/from (jt.f/parse %1 %2))) + +(conversion! Number LocalDate + #(LocalDate/of (int %) 1 1)) + +(conversion! Number LocalTime + #(LocalTime/of (int %) 0)) + +(conversion! Number LocalDateTime + #(LocalDateTime/of (int %) 1 1 0 0)) + +(conversion! [Number Number] LocalDate + #(LocalDate/of (int %1) (int %2) 1)) + +(conversion! [Number Number] LocalTime + #(LocalTime/of (int %1) (int %2))) + +(conversion! [Number Number] LocalDateTime + #(LocalDateTime/of (int %1) (int %2) 1 0 0)) + +(conversion! [Number Number Number] LocalDate + #(LocalDate/of (int %1) (int %2) (int %3))) + +(conversion! [Number Number Number] LocalTime + #(LocalTime/of (int %1) (int %2) (int %3))) + +(conversion! [Number Number Number] LocalDateTime + #(LocalDateTime/of (int %1) (int %2) (int %3) 0 0)) + +(conversion! [Instant ZoneId] LocalDateTime + (fn [^Instant i, ^ZoneId z] + (LocalDateTime/ofInstant i z))) + +(conversion! [LocalDate LocalTime] LocalDateTime + (fn [^LocalDate dt, ^LocalTime tm] + (LocalDateTime/of dt tm))) + +(conversion! LocalDateTime [LocalDate LocalTime] + (fn [^LocalDateTime ldt] + [(.toLocalDate ldt) (.toLocalTime ldt)])) + +(conversion! CharSequence java.time.format.DateTimeFormatter + jt.f/formatter) + diff --git a/src/java_time/potemkin/namespaces.clj b/src/java_time/potemkin/namespaces.clj new file mode 100644 index 0000000..0ef11cd --- /dev/null +++ b/src/java_time/potemkin/namespaces.clj @@ -0,0 +1,118 @@ +; The MIT License (MIT) +; +; Copyright (c) 2013 Zachary Tellman +; +; Permission is hereby granted, free of charge, to any person obtaining a copy +; of this software and associated documentation files (the "Software"), to deal +; in the Software without restriction, including without limitation the rights +; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +; copies of the Software, and to permit persons to whom the Software is +; furnished to do so, subject to the following conditions: +; +; The above copyright notice and this permission notice shall be included in +; all copies or substantial portions of the Software.) +; +; Copied from https://github.com/ztellman/potemkin/blob/master/src/potemkin/namespaces.clj +; to avoid having a dependency +(ns java-time.potemkin.namespaces) + +(defn link-vars + "Makes sure that all changes to `src` are reflected in `dst`." + [src dst] + (add-watch src dst + (fn [_ src old new] + (alter-var-root dst (constantly @src)) + (alter-meta! dst merge (dissoc (meta src) :name))))) + +(defmacro import-fn + "Given a function in another namespace, defines a function with the + same name in the current namespace. Argument lists, doc-strings, + and original line-numbers are preserved." + ([sym] + `(import-fn ~sym nil)) + ([sym name] + (let [vr (resolve sym) + m (meta vr) + n (or name (:name m)) + arglists (:arglists m) + protocol (:protocol m)] + (when-not vr + (throw (IllegalArgumentException. (str "Don't recognize " sym)))) + (when (:macro m) + (throw (IllegalArgumentException. + (str "Calling import-fn on a macro: " sym)))) + + `(do + (def ~(with-meta n {:protocol protocol}) (deref ~vr)) + (alter-meta! (var ~n) merge (dissoc (meta ~vr) :name)) + (link-vars ~vr (var ~n)) + ~vr)))) + +(defmacro import-macro + "Given a macro in another namespace, defines a macro with the same + name in the current namespace. Argument lists, doc-strings, and + original line-numbers are preserved." + ([sym] + `(import-macro ~sym nil)) + ([sym name] + (let [vr (resolve sym) + m (meta vr) + n (or name (:name m)) + arglists (:arglists m)] + (when-not vr + (throw (IllegalArgumentException. (str "Don't recognize " sym)))) + (when-not (:macro m) + (throw (IllegalArgumentException. + (str "Calling import-macro on a non-macro: " sym)))) + `(do + (def ~n ~(resolve sym)) + (alter-meta! (var ~n) merge (dissoc (meta ~vr) :name)) + (.setMacro (var ~n)) + (link-vars ~vr (var ~n)) + ~vr)))) + +(defmacro import-def + "Given a regular def'd var from another namespace, defined a new var with the + same name in the current namespace." + ([sym] + `(import-def ~sym nil)) + ([sym name] + (let [vr (resolve sym) + m (meta vr) + n (or name (:name m)) + n (if (:dynamic m) (with-meta n {:dynamic true}) n) + nspace (:ns m)] + (when-not vr + (throw (IllegalArgumentException. (str "Don't recognize " sym)))) + `(do + (def ~n @~vr) + (alter-meta! (var ~n) merge (dissoc (meta ~vr) :name)) + (link-vars ~vr (var ~n)) + ~vr)))) + +(defmacro import-vars + "Imports a list of vars from other namespaces." + [& syms] + (let [unravel (fn unravel [x] + (if (sequential? x) + (->> x + rest + (mapcat unravel) + (map + #(symbol + (str (first x) + (when-let [n (namespace %)] + (str "." n))) + (name %)))) + [x])) + syms (mapcat unravel syms)] + `(do + ~@(map + (fn [sym] + (let [vr (resolve sym) + m (meta vr)] + (cond + (:macro m) `(import-macro ~sym) + (:arglists m) `(import-fn ~sym) + :else `(import-def ~sym)))) + syms)))) diff --git a/src/java_time/potemkin/util.clj b/src/java_time/potemkin/util.clj new file mode 100644 index 0000000..a1f9f74 --- /dev/null +++ b/src/java_time/potemkin/util.clj @@ -0,0 +1,77 @@ +; The MIT License (MIT) +; +; Copyright (c) 2013 Zachary Tellman +; +; Permission is hereby granted, free of charge, to any person obtaining a copy +; of this software and associated documentation files (the "Software"), to deal +; in the Software without restriction, including without limitation the rights +; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +; copies of the Software, and to permit persons to whom the Software is +; furnished to do so, subject to the following conditions: +; +; The above copyright notice and this permission notice shall be included in +; all copies or substantial portions of the Software.) +; +; Partly Copied from https://github.com/ztellman/potemkin/blob/master/src/potemkin/util.clj +; to avoid having a dependency +(ns java-time.potemkin.util + (:refer-clojure :exclude (vector)) + (:require [clj-tuple :refer (vector)]) + (:import [java.util.concurrent ConcurrentHashMap])) + +;;; fast-memoize + +(definline re-nil [x] + `(let [x# ~x] + (if (identical? ::nil x#) nil x#))) + +(definline de-nil [x] + `(let [x# ~x] + (if (nil? x#) ::nil x#))) + +(defmacro memoize-form [m f & args] + `(let [k# (vector ~@args)] + (let [v# (.get ~m k#)] + (if-not (nil? v#) + (re-nil v#) + (let [v# (de-nil (~f ~@args))] + (re-nil (or (.putIfAbsent ~m k# v#) v#))))))) + +(defn fast-memoize + "A version of `memoize` which has equivalent behavior, but is faster." + [f] + (let [m (ConcurrentHashMap.)] + (fn + ([] + (memoize-form m f)) + ([x] + (memoize-form m f x)) + ([x y] + (memoize-form m f x y)) + ([x y z] + (memoize-form m f x y z)) + ([x y z w] + (memoize-form m f x y z w)) + ([x y z w u] + (memoize-form m f x y z w u)) + ([x y z w u v] + (memoize-form m f x y z w u v)) + ([x y z w u v & rest] + (let [k (list* x y z w u v rest)] + (let [v (.get ^ConcurrentHashMap m k)] + (if-not (nil? v) + (re-nil v) + (let [v (de-nil (apply f k))] + (or (.putIfAbsent m k v) v))))))))) + +(defmacro doit + "A version of doseq that doesn't emit all that inline-destroying chunked-seq code." + [[x it] & body] + (let [it-sym (gensym "iterable")] + `(let [~it-sym ~it + it# (.iterator ~(with-meta it-sym {:tag "Iterable"}))] + (loop [] + (when (.hasNext it#) + (let [~x (.next it#)] + ~@body) + (recur)))))) diff --git a/src/java_time/properties.clj b/src/java_time/properties.clj new file mode 100644 index 0000000..4387d69 --- /dev/null +++ b/src/java_time/properties.clj @@ -0,0 +1,166 @@ +(ns java-time.properties + (:require [java-time.core :as jt.c] + [java-time.util :as jt.u]) + (:import [java.time.temporal + TemporalField IsoFields ChronoField JulianFields + TemporalUnit ChronoUnit])) + +(defn- property->key [p] + (keyword (jt.u/dashize (str p)))) + +(defn- ->map [xs] + (zipmap (->> xs (map property->key)) + xs)) + +;;;;;;;;; Field/Unit groups + +(deftype FieldGroup [group-id field-map] + jt.c/HasFields + (fields [_] field-map) + (field* [_ k] (k field-map)) + Object + (toString [_] group-id)) + +(deftype UnitGroup [group-id unit-map] + jt.c/HasUnits + (units [_] unit-map) + (unit* [_ k] (k unit-map)) + Object + (toString [_] group-id)) + +;;;;;;;;; UNIT + +(def iso-units + (vals (jt.u/get-static-fields-of-type IsoFields TemporalUnit))) + +(def chrono-units + (vals (jt.u/get-static-fields-of-type ChronoUnit TemporalUnit))) + +(def predefined-units + (concat iso-units chrono-units)) + +(def unit-groups + {:predefined (->map predefined-units) + :iso (->map iso-units) + :chrono (->map chrono-units)}) + +(def ^:dynamic *units* (UnitGroup. :predefined (:predefined unit-groups))) + +(extend-type TemporalUnit + jt.c/KnowsTimeBetween + (time-between [u t1 t2] + (.between u ^Temporal t1, ^Temporal t2)) + + jt.c/Supporting + (supports? [u t] + (.isSupportedBy u ^Temporal t))) + +(defn unit? + "True if this is a `TemporalUnit`." + [o] (instance? TemporalUnit o)) + +(defn ^TemporalUnit get-unit [o] + (cond (unit? o) o + + (keyword? o) + (jt.c/unit* *units* o))) + +(defn ^TemporalUnit get-unit-checked [o] + (if-let [u (get-unit o)] + u + (throw (NullPointerException. (str "No temporal unit found for " o "!"))))) + +(defn unit-key [o] + (cond (keyword? o) + o + + (unit? o) + (property->key o))) + +;;;;;;;;; FIELD + +(def iso-fields + (vals (jt.u/get-static-fields-of-type IsoFields TemporalField))) + +(def julian-fields + (vals (jt.u/get-static-fields-of-type JulianFields TemporalField))) + +(def chrono-fields + (vals (jt.u/get-static-fields-of-type ChronoField TemporalField))) + +(def predefined-fields + (concat iso-fields chrono-fields julian-fields)) + +;; There is another implementation of fields - WeekFields, which is dynamic + +(def field-groups + {:predefined (->map predefined-fields) + :iso (->map iso-fields) + :julian (->map julian-fields) + :chrono (->map chrono-fields)}) + +(def ^:dynamic *fields* (FieldGroup. :predefined (:predefined field-groups))) + +(extend-type TemporalField + jt.c/Supporting + (supports? [f t] + (.isSupportedBy f ^TemporalAccessor t))) + +(extend TemporalField + jt.c/ReadableRangeProperty + (assoc jt.c/readable-range-property-fns + :range (fn [^TemporalField f] (.range f)))) + +(defn field? + "True if this is a `TemporalField`." + [o] (instance? TemporalField o)) + +(defn ^TemporalField get-field [o] + (cond (field? o) + o + + (keyword? o) + (jt.c/field* *fields* o))) + +(defn field-key [o] + (cond (keyword? o) + o + + (field? o) + (property->key o))) + +(defn ^TemporalUnit unit + "Returns a `TemporalUnit` for the given key `k` or extracts the field from + the given temporal `entity`. + + You can see predefined units via `java-time.repl/show-units`. + + If you want to make your own custom TemporalUnits resolvable, you need to rebind the + `java-time.properties/*units*` to a custom `java-time.properties.UnitGroup`." + ([k] (get-unit k)) + ([entity k] (jt.c/unit* entity k))) + +(defn ^TemporalField field + "Returns a `TemporalField` for the given key `k` or extracts the field from + the given temporal `entity`. + + You can see predefined fields via `java-time.repl/show-fields`. + + If you want to make your own custom TemporalFields resolvable, you need to rebind the + `java-time.properties/*fields*` to a custom `java-time.properties.FieldGroup`." + ([k] (get-field k)) + ([entity k] (jt.c/field* entity k))) + +(extend-type clojure.lang.Keyword + jt.c/KnowsTimeBetween + (time-between [k t1 t2] + (jt.c/time-between (or (get-field k) (get-unit k)) t1 t2)) + + jt.c/Supporting + (supports? [k t] + (jt.c/supports? (or (get-field k) (get-unit k)) t))) + +(extend clojure.lang.Keyword + jt.c/ReadableRangeProperty + (assoc jt.c/readable-range-property-fns + :range (fn [k] (jt.c/range (get-field k))))) diff --git a/src/java_time/repl.clj b/src/java_time/repl.clj new file mode 100644 index 0000000..cd869af --- /dev/null +++ b/src/java_time/repl.clj @@ -0,0 +1,28 @@ +(ns java-time.repl + (:require [java-time.adjuster :as j.adj] + [java-time.properties :as j.p] + [java-time.format :as jt.f] + [java-time.zone :as jt.z] + [java-time.defconversion :as jt.dc] + [clojure.pprint :as pprint])) + +(defn show-adjusters [] + (pprint/pprint (sort (keys j.adj/predefined-adjusters)))) + +(defn show-units [] + (pprint/pprint (sort (keys (:predefined j.p/unit-groups))))) + +(defn show-fields [] + (pprint/pprint (sort (keys (:predefined j.p/field-groups))))) + +(defn show-formatters [] + (pprint/pprint (sort (keys jt.f/predefined-formatters)))) + +(defn show-timezones [] + (pprint/pprint (sort (jt.z/available-zone-ids)))) + +(defn show-graph [] + (pprint/pprint (.m-by-arity @jt.dc/graph))) + +(defn show-path [from to] + (jt.dc/get-path from to)) diff --git a/src/java_time/seqs.clj b/src/java_time/seqs.clj new file mode 100644 index 0000000..5e1b908 --- /dev/null +++ b/src/java_time/seqs.clj @@ -0,0 +1,24 @@ +(ns java-time.seqs + (:refer-clojure :exclude [iterate])) + +(defn- partialr [f & args] + (fn [a & as] + (apply f a (concat as args)))) + +(defn iterate + "Returns a lazy sequence of `initial` , `(apply f initial v vs)`, etc. + + Useful when you want to produce a sequence of temporal entities, for + example: + + (iterate plus (days 0) 1) + => (# # # ...) + + (iterate plus (local-date 2010 1 1) (years 1)) + => (# # ...) + + (iterate adjust (local-date 2010 1 1) :next-working-day) + => (# # ...)" + [f initial v & vs] + (clojure.core/iterate + (apply partialr f v vs) initial)) diff --git a/src/java_time/single_field.clj b/src/java_time/single_field.clj new file mode 100644 index 0000000..ccd1afb --- /dev/null +++ b/src/java_time/single_field.clj @@ -0,0 +1,224 @@ +(ns java-time.single-field + (:require [clojure.string :as string] + [java-time.zone :as jt.z] + [java-time.format :as jt.f] + [java-time.core :as jt.c] + [java-time.util :as jt.u] + [java-time.clock :as jt.clock] + [java-time.defconversion :refer (conversion!)]) + (:import [java.time.temporal TemporalAccessor TemporalAmount TemporalUnit ChronoUnit] + [java.time.format DateTimeFormatter] + [java.time Clock Year Month YearMonth MonthDay DayOfWeek ZoneId Instant])) + +(defn- ^long get-only-unit-value [^TemporalAmount a, ^TemporalUnit u] + (let [non-zero-units + (->> (.getUnits a) + (map (fn [^TemporalUnit tu] (vector tu (.get a tu)))) + (filter (fn [[_ uv]] (not (zero? uv))))) + [our-unit our-value] (first (filter (fn [[tu]] (= tu u)) non-zero-units))] + (when-not our-unit + (throw (java.time.temporal.UnsupportedTemporalTypeException. + (format "No unit: %s found in %s!" u a)))) + (when (> (count non-zero-units) 1) + (throw (java.time.temporal.UnsupportedTemporalTypeException. + (format "Cannot use: %s, expected only %s to be non-zero!" a u)))) + (long our-value))) + +(defmacro enumerated-entity [tp doc & {:keys [unit]}] + (let [fname (with-meta (symbol (jt.u/dashize (str tp))) {:tag tp}) + fields (symbol (str fname "-fields"))] + `(do + (def ~fields + (->> (jt.u/get-static-fields-of-type ~tp TemporalAccessor) + (vals) + (map (fn [m#] [(keyword (string/lower-case (str m#))) m#])) + (into {}))) + + (defn ^{:doc ~(str "True if `" tp "`.")} ~(symbol (str fname "?")) + [o#] + (instance? ~tp o#)) + + (conversion! ~tp Number jt.c/value) + + (defn ~fname ~doc + ([] (. ~tp from (jt.z/zoned-date-time))) + ([v#] (cond (keyword? v#) + (v# ~fields) + + (number? v#) + (. ~tp of (int v#)) + + (instance? TemporalAccessor v#) + (. ~tp from v#))) + ([fmt# arg#] + (~fname (jt.f/parse fmt# arg#)))) + + (extend-type ~tp + jt.c/Ordered + (single-after? [d# o#] + (> (.getValue d#) (.getValue (~fname o#)))) + (single-before? [d# o#] + (< (.getValue d#) (.getValue (~fname o#))))) + + ;; Enum-based entities do not implement `Temporal`, thus we don't have an easy + ;; option to add/subtract a TemporalAmount. + ~@(when unit + (for [[protocol proto-op op] [[`jt.c/Plusable 'seq-plus 'plus] + [`jt.c/Minusable 'seq-minus 'minus]]] + (let [typed-arg (with-meta (gensym) {:tag tp})] + `(extend-type ~tp ~protocol + (~proto-op [o# os#] + (reduce + (fn [~typed-arg v#] + (cond (number? v#) + (. ~typed-arg ~op (long v#)) + + (instance? TemporalAmount v#) + (. ~typed-arg ~op (get-only-unit-value v# ~unit)))) + o# os#))))))))) + +(defmacro single-field-entity [tp doc & {:keys [parseable?]}] + (let [fname (with-meta (symbol (jt.u/dashize (str tp))) {:tag tp}) + arg (gensym)] + `(do + (defn ^{:doc ~(str "True if `" tp "`.")} ~(symbol (str fname "?")) + [o#] + (instance? ~tp o#)) + + (conversion! ~tp Number jt.c/value) + + (defn ~fname ~doc + ([] (. ~tp from (jt.z/zoned-date-time))) + ([~arg] (cond (number? ~arg) + (. ~tp of (int ~arg)) + + (instance? TemporalAccessor ~arg) + (. ~tp from ~arg) + + (instance? Clock ~arg) + (. ~tp now ~(with-meta arg {:tag `Clock})) + + (instance? ZoneId ~arg) + (. ~tp now ~(with-meta arg {:tag `ZoneId})) + + ~@(when parseable? + `[(string? ~arg) + (try (. ~tp parse ~arg) + (catch java.time.format.DateTimeParseException _# + (. ~tp now (jt.z/zone-id ~arg))))]))) + ([fmt# arg#] + (~fname (jt.f/parse fmt# arg#)))) + + (extend-type ~tp + jt.c/Ordered + (single-after? [d# o#] + (> (.getValue d#) (.getValue (~fname o#)))) + (single-before? [d# o#] + (< (.getValue d#) (.getValue (~fname o#)))))))) + +(defmacro two-field-entity [tp doc & {:keys [major-field-types major-field-ctor + minor-field-ctor minor-field-default]}] + (let [fname (with-meta (symbol (jt.u/dashize (str tp))) {:tag tp}) + arg (gensym)] + `(do + (defn ^{:doc ~(str "True if `" tp "`.") } ~(symbol (str fname "?")) + [o#] + (instance? ~tp o#)) + + (defn ~fname ~doc + ([] (. ~tp from (jt.z/zoned-date-time))) + ([~arg] (cond (some (fn [x#] (instance? x# ~arg)) ~major-field-types) + (. ~tp of (~major-field-ctor ~arg) ~minor-field-default) + + (instance? TemporalAccessor ~arg) + (. ~tp from ~arg) + + (instance? Clock ~arg) + (. ~tp now ~(with-meta arg {:tag `Clock})) + + (instance? ZoneId ~arg) + (. ~tp now ~(with-meta arg {:tag `ZoneId})) + + (string? ~arg) + (try (. ~tp parse ~arg) + (catch java.time.format.DateTimeParseException _# + (. ~tp now (jt.z/zone-id ~arg)))) + + :else (. ~tp of (~major-field-ctor ~arg) ~minor-field-default))) + ([a# b#] + (if (and (or (instance? DateTimeFormatter a#) (string? a#)) (string? b#)) + (~fname (jt.f/parse a# b#)) + (. ~tp of (~major-field-ctor a#) (~minor-field-ctor b#))))) + + (extend-type ~tp + jt.c/Ordered + (single-after? [d# o#] + (.isAfter d# o#)) + (single-before? [d# o#] + (.isBefore d# o#)))))) + +(enumerated-entity DayOfWeek + "Returns the `DayOfWeek` for the given day keyword name (e.g. `:monday`), + ordinal or entity. Current day if no arguments given." + :unit ChronoUnit/DAYS) + +(enumerated-entity Month + "Returns the `Month` for the given month keyword name (e.g. `:january`), + ordinal or entity. Current month if no arguments given." + :unit ChronoUnit/MONTHS) + +(single-field-entity Year + "Returns the `Year` for the given entity, string, clock, zone or number. + Current year if no arguments given." + :parseable? true) + +(two-field-entity MonthDay + "Returns the `MonthDay` for the given entity, string, clock, zone or + month/day combination. Current month-day if no arguments given." + :major-field-ctor month + :major-field-types [Month Number] + :minor-field-ctor (comp int jt.c/value) + :minor-field-default 1) + +(two-field-entity YearMonth + "Returns the `YearMonth` for the given entity, string, clock, zone or + month/day combination. Current year-month if no arguments given." + :major-field-ctor (comp int jt.c/value) + :major-field-types [Year Number] + :minor-field-ctor month + :minor-field-default 1) + +;;;;;;;;;; Threeten Extra + +;; Do not use Months/Days/Weeks/Years as already covered by java.time.Period +(jt.u/when-threeten-extra + (import [org.threeten.extra AmPm DayOfMonth DayOfYear Quarter YearQuarter]) + + (enumerated-entity AmPm + "Returns the `AmPm` for the given keyword name (`:am` or `:pm`), + ordinal or entity. Current AM/PM if no arguments given.") + + (enumerated-entity Quarter + "Returns the `Quarter` for the given quarter keyword name (e.g. `:q1`), + ordinal or entity. Current quarter if no arguments given." + :unit java.time.temporal.IsoFields/QUARTER_YEARS) + + (single-field-entity DayOfMonth + "Returns the `DayOfMonth` for the given entity, clock, zone or day of month. + Current day of month if no arguments given.") + + (single-field-entity DayOfYear + "Returns the `DayOfYear` for the given entity, clock, zone or day of year. + Current day of year if no arguments given.") + + (defn ^Integer year-to-int [x] + (if (number? x) (int x) + (.getValue ^Year x))) + + (two-field-entity YearQuarter + "Returns the `YearQuarter` for the given entity, clock, zone or year with quarter. + Current year quarter if no arguments given." + :major-field-ctor year-to-int + :major-field-types [Year Number] + :minor-field-ctor quarter + :minor-field-default 1)) diff --git a/src/java_time/sugar.clj b/src/java_time/sugar.clj new file mode 100644 index 0000000..5af7666 --- /dev/null +++ b/src/java_time/sugar.clj @@ -0,0 +1,19 @@ +(ns java-time.sugar + (:require [java-time.core :as jt.c] + [java-time.util :as jt.u])) + +(doseq [[day-name day-number] + [['monday 1] ['tuesday 2] ['wednesday 3] ['thursday 4] ['friday 5] + ['saturday 6] ['sunday 7]]] + (eval `(defn ~(symbol (str day-name '?)) + ~(str "Returns true if the given time entity with the\n" + " `day-of-week` property falls on a " day-name ".") + [o#] (if-let [p# (jt.c/property o# :day-of-week)] + (= (jt.c/value p#) ~day-number) + (throw (java.time.DateTimeException. (str "Day of week unsupported for: " (type o#)))))))) + +(defn weekend? [dt] + (or (saturday? dt) (sunday? dt))) + +(defn weekday? [dt] + (not (weekend? dt))) diff --git a/src/java_time/temporal.clj b/src/java_time/temporal.clj new file mode 100644 index 0000000..5ef1dd9 --- /dev/null +++ b/src/java_time/temporal.clj @@ -0,0 +1,374 @@ +(ns java-time.temporal + (:require [clojure.string :as string] + [java-time.core :as jt.c :refer (value)] + [java-time.util :as jt.u] + [java-time.properties :as jt.p] + [java-time.format :as jt.f] + [java-time.clock :as jt.clock] + [java-time.defconversion :refer (deffactory conversion!)]) + (:import [java.time.temporal Temporal TemporalAccessor ValueRange + TemporalField TemporalUnit TemporalAmount ChronoField IsoFields] + [java.time.format DateTimeFormatter] + [java.time.chrono Chronology] + [java.time DateTimeException Clock + Period Duration MonthDay DayOfWeek Month Year + ZoneOffset Instant])) + +(def writable-range-property-fns + {:with-min-value (fn [p] (jt.c/with-value p (jt.c/min-value p))) + :with-largest-min-value (fn [p] (jt.c/with-value p (jt.c/largest-min-value p))) + :with-smallest-max-value (fn [p] (jt.c/with-value p (jt.c/smallest-max-value p))) + :with-max-value (fn [p] (jt.c/with-value p (jt.c/max-value p)))}) + +(defmacro value-property [java-type range-field & + {:keys [with-value-fn-sym get-value-fn-sym] + :or {with-value-fn-sym 'of + get-value-fn-sym 'getValue}}] + (let [java-type-arg (with-meta (gensym) {:tag java-type})] + `(do + (extend-type ~java-type + jt.c/ReadableProperty + (value [d#] + (. d# ~get-value-fn-sym)) + + jt.c/WritableProperty + (with-value [_# v#] + (. ~java-type ~with-value-fn-sym v#))) + + (extend ~java-type + jt.c/ReadableRangeProperty + (assoc jt.c/readable-range-property-fns + :range (fn [~java-type-arg] + (.range ~java-type-arg ~range-field))) + + jt.c/WritableRangeProperty + writable-range-property-fns)))) + +(value-property DayOfWeek ChronoField/DAY_OF_WEEK) +(value-property Month ChronoField/MONTH_OF_YEAR) +(value-property Year ChronoField/YEAR_OF_ERA) +(value-property ZoneOffset ChronoField/OFFSET_SECONDS + :with-value-fn-sym ofTotalSeconds + :get-value-fn-sym getTotalSeconds) + +(jt.u/when-threeten-extra + (import [org.threeten.extra AmPm DayOfMonth DayOfYear Quarter YearQuarter]) + (value-property DayOfMonth ChronoField/DAY_OF_MONTH) + (value-property DayOfYear ChronoField/DAY_OF_YEAR)) + +;;;;; FIELD PROPERTY + +(defn- get-field-property-range [^TemporalAccessor ta, ^TemporalField field] + (.range ta field)) + +(defn- get-long-property-value [^TemporalAccessor ta, ^TemporalField field] + (.getLong ta field)) + +(defn- quarter->month [q] + (Math/min 12 (Math/max 1 (long (inc (* 3 (dec q))))))) + +(defrecord MonthDayFieldProperty [^MonthDay o, ^TemporalField field] + jt.c/WritableProperty + (with-value [_ v] + (condp = field + ChronoField/DAY_OF_MONTH (.withDayOfMonth o v) + ChronoField/MONTH_OF_YEAR (.withMonth o v) + IsoFields/QUARTER_OF_YEAR (.withMonth o (quarter->month v))))) + +(alter-meta! #'->MonthDayFieldProperty assoc :private true) +(alter-meta! #'map->MonthDayFieldProperty assoc :private true) + +(defrecord DayOfWeekFieldProperty [^DayOfWeek o, ^TemporalField field] + jt.c/WritableProperty + (with-value [_ v] + (condp = field + ChronoField/DAY_OF_WEEK (DayOfWeek/of v)))) + +(alter-meta! #'->DayOfWeekFieldProperty assoc :private true) +(alter-meta! #'map->DayOfWeekFieldProperty assoc :private true) + +(defrecord MonthFieldProperty [^Month o, ^TemporalField field] + jt.c/WritableProperty + (with-value [_ v] + (condp = field + ChronoField/MONTH_OF_YEAR (Month/of v) + IsoFields/QUARTER_OF_YEAR (Month/of (quarter->month v))))) + +(alter-meta! #'->MonthFieldProperty assoc :private true) +(alter-meta! #'map->MonthFieldProperty assoc :private true) + +(defrecord ZoneOffsetFieldProperty [^ZoneOffset o, ^TemporalField field] + jt.c/WritableProperty + (with-value [_ v] + (condp = field + ChronoField/OFFSET_SECONDS (ZoneOffset/ofTotalSeconds v)))) + +(alter-meta! #'->ZoneOffsetFieldProperty assoc :private true) +(alter-meta! #'map->ZoneOffsetFieldProperty assoc :private true) + +(defrecord TemporalFieldProperty [^Temporal o, ^TemporalField field] + jt.c/WritableProperty + (with-value [_ v] (.with o field v))) + +(alter-meta! #'->TemporalFieldProperty assoc :private true) +(alter-meta! #'map->TemporalFieldProperty assoc :private true) + +(defmacro field-property [java-type has-range?] + (let [java-type-arg (with-meta (gensym) {:tag java-type})] + `(do + (extend ~java-type + jt.c/ReadableProperty + {:value (fn [~java-type-arg] + (get-long-property-value (.o ~java-type-arg) + (.field ~java-type-arg)))}) + + ~(when has-range? + `(extend ~java-type + jt.c/ReadableRangeProperty + (assoc jt.c/readable-range-property-fns + :range (fn [~java-type-arg] + (get-field-property-range (.o ~java-type-arg) + (.field ~java-type-arg)))) + + jt.c/WritableRangeProperty + writable-range-property-fns))))) + +(field-property DayOfWeekFieldProperty true) +(field-property MonthFieldProperty true) +(field-property MonthDayFieldProperty true) +(field-property TemporalFieldProperty true) +(field-property ZoneOffsetFieldProperty true) + +;;;;; FACTORY + +(defprotocol PropertyFactory + (mk-property [factory entity prop-key prop-obj])) + +(def default-field-property-factory + (reify PropertyFactory + (mk-property [_ e _ field] + (condp instance? e + Temporal (TemporalFieldProperty. e field) + Month (MonthFieldProperty. e field) + DayOfWeek (DayOfWeekFieldProperty. e field) + MonthDay (MonthDayFieldProperty. e field) + ZoneOffset (ZoneOffsetFieldProperty. e field))))) + +(def ^:dynamic *field-property-factory* default-field-property-factory) + +;;;;; ACCESSOR + +(extend-type TemporalAccessor + jt.c/Supporting + (supports? [o k] + (.isSupported o (jt.p/field k))) + + jt.c/HasFields + (field* [o k] + (when-let [f (jt.p/field k)] + (when (jt.c/supports? o f) + f))) + + (fields [o] + (let [fs (jt.c/fields jt.p/*fields*)] + (loop [[k f] (first fs) + r (rest fs) + res (transient {})] + (if f + (recur (first r) (rest r) + (if (jt.c/supports? o f) + (assoc! res k f) + res)) + (persistent! res))))) + + jt.c/HasProperties + (properties [o] + (jt.u/map-kv + (fn [k p] [k (mk-property *field-property-factory* o k p)]) + (jt.c/fields o))) + + (property [o k] + (let [f-k (jt.p/field-key k)] + (if-let [f (jt.c/field* o k)] + (mk-property *field-property-factory* o f-k f) + (throw (DateTimeException. (str "Property " k " doesn't exist in [" o "]!"))))))) + +;;;;;;;;; RANGE + +(defn ^ValueRange value-range + "Creates a `ValueRange` given the `min` and `max` amounts or a map of + `:min-smallest`, `:max-smallest`, `:min-largest` and `:max-largest`." + ([min max] + (value-range {:min-smallest min, :max-smallest max + :min-largest min, :max-largest max})) + ([{:keys [min-smallest min-largest max-smallest max-largest]}] + (ValueRange/of min-smallest min-largest max-smallest max-largest))) + +;;;;;;;;; AMOUNT + +(defrecord TemporalAmountUnitProperty [^TemporalAmount ta, ^TemporalUnit unit] + jt.c/ReadableProperty + (value [_] + (.get ta unit))) + +(alter-meta! #'->TemporalAmountUnitProperty assoc :private true) +(alter-meta! #'map->TemporalAmountUnitProperty assoc :private true) + +(defrecord PeriodUnitProperty [^Period p, unit-key] + jt.c/ReadableProperty + (value [_] + (case unit-key + :years (.getYears p) + :months (.getMonths p) + :days (.getDays p))) + + jt.c/WritableProperty + (with-value [_ v] + (case unit-key + :years (.withYears p v) + :months (.withMonths p v) + :days (.withDays p v)))) + +(alter-meta! #'->PeriodUnitProperty assoc :private true) +(alter-meta! #'map->PeriodUnitProperty assoc :private true) + +(defrecord DurationUnitProperty [^Duration d, unit-key] + jt.c/ReadableProperty + (value [_] + (case unit-key + :seconds (.getSeconds d) + :nanos (.getNano d))) + + jt.c/WritableProperty + (with-value [_ v] + (case unit-key + :seconds (.withSeconds d v) + :nanos (.withNanos d v)))) + +(alter-meta! #'->DurationUnitProperty assoc :private true) +(alter-meta! #'map->DurationUnitProperty assoc :private true) + +(def default-unit-property-factory + (reify PropertyFactory + (mk-property [_ e unit-key unit] + (condp instance? e + Period (PeriodUnitProperty. e unit-key) + Duration (DurationUnitProperty. e unit-key) + TemporalAmount (TemporalAmountUnitProperty. e unit))))) + +(def ^:dynamic *unit-property-factory* default-unit-property-factory) + +(extend-type TemporalAmount + jt.c/Supporting + (supports? [o k] + (not (nil? (jt.c/unit* o (jt.p/get-unit k))))) + + jt.c/HasUnits + (unit* [o k] + (when-let [u (jt.p/get-unit k)] + (first (filter #(= u %) (.getUnits o))))) + + (units [o] + (let [[u & us] (.getUnits o)] + (loop [u u, us us, res (transient {})] + (if u + (recur (first us) (rest us) + (assoc! res (jt.p/unit-key u) u)) + (persistent! res))))) + + jt.c/HasProperties + (properties [o] + (jt.u/map-kv + (fn [k p] [k (mk-property *unit-property-factory* o k p)]) + (jt.c/units o))) + + (property [o k] + (let [u-k (jt.p/unit-key k)] + (if-let [u (jt.c/unit* o k)] + (mk-property *unit-property-factory* o u-k u) + (throw (DateTimeException. (str "Property " k " doesn't exist in [" o "]!"))))))) + +;;;;;;;;; TEMPORAL + +(defn ^Temporal t-plus [^Temporal acc, ^TemporalAmount o] + (.plus acc o)) + +(defn ^Temporal t-minus [^Temporal acc, ^TemporalAmount o] + (.minus acc o)) + +(extend-type Temporal + jt.c/Plusable + (seq-plus [o os] + (reduce t-plus o os)) + + jt.c/Minusable + (seq-minus [o os] + (reduce t-minus o os)) + + jt.c/KnowsTimeBetween + (time-between [o e u] + (.until o ^Temporal e (jt.p/get-unit u))) + + jt.c/KnowsIfLeap + (leap? [o] + (when-let [year (-> (jt.c/property o :year) + (jt.c/value))] + (if (satisfies? jt.c/HasChronology o) + (.isLeapYear (jt.c/chronology o) year) + (Year/isLeap year)))) + + jt.c/As + (as* [o k] + (jt.c/value (jt.c/property o k)))) + +;;;;;; Instant + +(conversion! Clock Instant + (fn [^Clock c] + (Instant/now c))) + +(conversion! java.util.Date Instant + (fn [^java.util.Date dt] + (.toInstant dt))) + +(conversion! java.util.Calendar Instant + (fn [^java.util.Calendar c] + (.toInstant c))) + +(conversion! CharSequence Instant + (fn [^CharSequence s] + (Instant/parse s)) + 2) + +(conversion! Number Instant + (fn [^Number m] + (Instant/ofEpochMilli (long m)))) + +(conversion! [Number Number] Instant + (fn [^Number s, ^Number m] + (Instant/ofEpochSecond (long s) (long m)))) + +(conversion! [DateTimeFormatter CharSequence] Instant + #(Instant/from (jt.f/parse %1 %2))) + +(deffactory instant + "Creates an `Instant`. The following arguments are supported: + + * no arguments - current instant + * one argument + + clock + + java.util.Date/Calendar + + another temporal entity + + string representation + + millis from epoch + * two arguments + + formatter (format) and a string + + seconds from epoch and a nano adjustment" + :returns Instant + :implicit-arities [1 2] + ([] (jt.clock/make #(Instant/now %)))) + +(extend-type Instant + jt.c/Truncatable + (truncate-to [o u] + (.truncatedTo o (jt.p/get-unit-checked u)))) diff --git a/src/java_time/util.clj b/src/java_time/util.clj new file mode 100644 index 0000000..64513d6 --- /dev/null +++ b/src/java_time/util.clj @@ -0,0 +1,52 @@ +(ns java-time.util + (:require [clojure.string :as string]) + (:import [java.lang.reflect Field])) + +(defn get-static-fields-of-type [^Class klass, ^Class of-type] + (->> (seq (.getFields klass)) + (map (fn [^Field f] + (when (.isAssignableFrom of-type (.getType f)) + [(.getName f) (.get f nil)])) ) + (keep identity) + (into {}))) + +(defn dashize [camelcase] + (let [words (re-seq #"([^A-Z]+|[A-Z]+[^A-Z]*)" camelcase)] + (string/join "-" (map (comp string/lower-case first) words)))) + +(defmacro if-threeten-extra [then-body else-body] + (if (try (Class/forName "org.threeten.extra.Temporals") + (catch Throwable e)) + `(do ~then-body) + `(do ~else-body))) + +(defmacro when-threeten-extra [& body] + (if (try (Class/forName "org.threeten.extra.Temporals") + (catch Throwable e)) + `(do ~@body))) + +(defmacro when-joda [& body] + (if (try (Class/forName "org.joda.time.DateTime") + (catch Throwable e)) + `(do ~@body))) + +;; From Medley, C Weavejester +(defn editable? [coll] + (instance? clojure.lang.IEditableCollection coll)) + +(defn reduce-map [f coll] + (if (editable? coll) + (persistent! (reduce-kv (f assoc!) (transient (empty coll)) coll)) + (reduce-kv (f assoc) (empty coll) coll))) + +(defn map-vals + "Maps a function over the values of an associative collection." + [f coll] + (reduce-map (fn [xf] (fn [m k v] (xf m k (f v)))) coll)) + +(defn map-kv + "Maps a function over the key/value pairs of an associate collection. Expects + a function that takes two arguments, the key and value, and returns the new + key and value as a collection of two elements." + [f coll] + (reduce-map (fn [xf] (fn [m k v] (let [[k v] (f k v)] (xf m k v)))) coll)) diff --git a/src/java_time/zone.clj b/src/java_time/zone.clj new file mode 100644 index 0000000..d8de623 --- /dev/null +++ b/src/java_time/zone.clj @@ -0,0 +1,382 @@ +(ns java-time.zone + (:require [java-time.core :as jt.c :refer (value)] + [java-time.temporal :as jt.t] + [java-time.util :as jt.u] + [java-time.amount :as jt.a] + [java-time.format :as jt.f] + [java-time.clock :as jt.clock] + [java-time.properties :as jt.p :refer (get-unit-checked)] + [java-time.defconversion :refer (conversion! deffactory)]) + (:import [java.time.temporal TemporalAccessor] + [java.time.format DateTimeFormatter] + [java.time Clock Instant LocalDate LocalTime LocalDateTime + ZoneId ZoneOffset OffsetDateTime OffsetTime ZonedDateTime])) + +;;;;; Zone Id + +(defn- to-hms [n] + (if (integer? n) + [n 0 0] + (let [h (int n) + m-n (* 60 (- n h)) + m (int m-n) + s (int (* 60 (- m-n m)))] + [h m s]))) + +(conversion! java.util.TimeZone ZoneId + (fn [^java.util.TimeZone z] + (.toZoneId z))) + +(conversion! CharSequence ZoneId + (fn [^CharSequence s] + (ZoneId/of s))) + +(conversion! [CharSequence ZoneOffset] ZoneId + (fn [^CharSequence s, ^ZoneOffset zo] + (ZoneId/ofOffset s zo))) + +(conversion! Number ZoneOffset + (fn [n] + (let [[h m s] (to-hms n)] + (ZoneOffset/ofHoursMinutesSeconds h m s)))) + +(defn- ^ZoneOffset clock->zone-offset [^Clock c] + (-> (.getZone c) + (.getRules) + (.getOffset (.instant c)))) + +(deffactory zone-offset + "Creates a `ZoneOffset` from a string identifier (e.g. \"+01\"), a number of + hours/hours and minutes/hours, minutes and seconds or extracts from another + temporal entity. + + Returns default system zone offset if no arguments provided." + :returns ZoneOffset + :implicit-arities [] + ([] (jt.clock/make clock->zone-offset)) + ([o] (cond (instance? ZoneOffset o) + o + + (instance? Clock o) + (clock->zone-offset o) + + (instance? java.time.temporal.TemporalAccessor o) + (ZoneOffset/from ^java.time.temporal.TemporalAccessor o) + + (string? o) + (ZoneOffset/of ^String o) + + (number? o) + (let [[h m s] (to-hms o)] + (zone-offset h m s)))) + ([h m] (ZoneOffset/ofHoursMinutes h m)) + ([h m s] (ZoneOffset/ofHoursMinutesSeconds h m s))) + +(deffactory zone-id + "Creates a `ZoneId` from a string identifier, `java.util.TimeZone` or extracts + from another temporal entity. + + Returns default system zone id if no arguments provided. + + Given two arguments will use the second as the offset." + :returns ZoneId + :implicit-arities [1 2] + ([] (jt.clock/make (fn [^Clock c] (.getZone c))))) + +(defn available-zone-ids [] + (ZoneId/getAvailableZoneIds)) + +;; offset date/time + +(deffactory offset-date-time + "Creates an `OffsetDateTime`. The following arguments are supported: + + * no arguments - current date-time with the default offset + * one argument + + clock + + zone id + + another temporal entity + + string representation + + offset + * two arguments + + formatter (format) and a string + + local date-time and an offset + + another temporal entity and an offset (preserves local time) + + year and an offset + * three arguments + + local date, local time and an offset + + year, month and an offset + * four up to eight arguments - last is always the offset" + :returns OffsetDateTime + :implicit-arities [1 2 3] + ([] (jt.clock/make (fn [^Clock c] (OffsetDateTime/now c)))) + ([y m d o] (offset-date-time y m d 0 o)) + ([y m d h o] (offset-date-time y m d h 0 o)) + ([y mo d h m o] (offset-date-time y mo d h m 0 o)) + ([y mo d h m s o] (offset-date-time y mo d h m s 0 o)) + ([y mo d h m s n o] + (OffsetDateTime/of + (int (value y)) (int (value mo)) (int (value d)) + (int h) (int m) (int s) (int n) (zone-offset o)))) + +(deffactory offset-time + "Creates an `OffsetTime`. The following arguments are supported: + + * no arguments - current time with the default offset + * one argument + + clock + + zone id + + another temporal entity + + string representation + + offset - produces local time with the given offset + * two arguments + + formatter (format) and a string + + local time and an offset + + instant and an offset + + hour and an offset + * three up to five arguments - last is always the offset" + :returns OffsetTime + :implicit-arities [1 2 3] + ([] (jt.clock/make (fn [^Clock c] (OffsetTime/now c)))) + ([h m s o] (offset-time h m s 0 o)) + ([h m s n o] + (OffsetTime/of h m s n (zone-offset o)))) + +(deffactory zoned-date-time + "Creates a `ZonedDateTime`. The following arguments are supported: + + * no arguments - current date-time in the default zone + * one argument + + clock + + zone id + + another temporal entity + + string representation + + year + * two arguments + + formatter and a string + + local date-time and a zone id + + year and a zone id + + year and month + * three arguments + + local date, local time and a zone id + + year, month and a zone id + + year, month and day + * four up to eight arguments - last is always the zone id + + If zone id is not specified, default zone id will be used. You can check the + default zone by invoking `(zone-id)`." + :returns ZonedDateTime + :implicit-arities [1 2 3] + ([] (jt.clock/make (fn [^Clock c] (ZonedDateTime/now c)))) + ([y m d o] (zoned-date-time y m d 0 o)) + ([y m d h o] (zoned-date-time y m d h 0 o)) + ([y mo d h m o] (zoned-date-time y mo d h m 0 o)) + ([y mo d h m s o] (zoned-date-time y mo d h m s 0 o)) + ([y mo d h m s n o] + (ZonedDateTime/of + (int (value y)) (int (value mo)) (int (value d)) + (int h) (int m) (int s) (int n) (zone-id o)))) + +(conversion! Clock ZonedDateTime + (fn [^Clock c] + (ZonedDateTime/now c))) + +(conversion! Clock OffsetDateTime + (fn [^Clock c] + (OffsetDateTime/now c))) + +(conversion! Clock OffsetTime + (fn [^Clock c] + (OffsetTime/now c))) + +(conversion! ZoneId ZonedDateTime + (fn [^ZoneId z] + (ZonedDateTime/now z)) + 2) + +(conversion! ZoneId OffsetDateTime + (fn [^ZoneId z] + (OffsetDateTime/now z)) + 2) + +(conversion! ZoneId OffsetTime + (fn [^ZoneId z] + (OffsetTime/now z)) + 2) + +(conversion! CharSequence ZonedDateTime + (fn [^CharSequence s] + (ZonedDateTime/parse s)) + 3) + +(conversion! CharSequence OffsetDateTime + (fn [^CharSequence s] + (OffsetDateTime/parse s)) + 2) + +(conversion! CharSequence OffsetTime + (fn [^CharSequence s] + (OffsetTime/parse s)) + 2) + +(conversion! ZoneOffset OffsetDateTime + (fn [^ZoneOffset zo] + (.withOffsetSameLocal (OffsetDateTime/now) zo)) + 4) + +(conversion! ZoneOffset OffsetTime + (fn [^ZoneOffset zo] + (.withOffsetSameLocal (OffsetTime/now) zo)) + 4) + +(conversion! ZonedDateTime [Instant ZoneId] + (fn [^ZonedDateTime zdt] + [(.toInstant zdt) (.getZone zdt)])) + +(conversion! OffsetDateTime [Instant ZoneOffset] + (fn [^OffsetDateTime odt] + [(.toInstant odt) (.getOffset odt)])) + +(conversion! OffsetTime [LocalTime ZoneOffset] + (fn [^OffsetTime odt] + [(.toLocalTime odt) (.getOffset odt)])) + +(conversion! OffsetTime OffsetDateTime + (fn [^OffsetTime ot] + (.atDate ot (LocalDate/now))) + 2) + +(conversion! [LocalDateTime ZoneOffset] OffsetDateTime + (fn [^LocalDateTime ldt, ^ZoneOffset zo] + (OffsetDateTime/of ldt zo))) + +(conversion! [LocalDateTime ZoneId] ZonedDateTime + (fn [^LocalDateTime ldt, ^ZoneId z] + (ZonedDateTime/of ldt z))) + +(conversion! [LocalTime ZoneOffset] OffsetTime + (fn [^LocalTime lt, ^ZoneOffset zo] + (OffsetTime/of lt zo))) + +(conversion! [Instant ZoneId] ZonedDateTime + (fn [^Instant i, ^ZoneId z] + (ZonedDateTime/ofInstant i z))) + +(conversion! [Instant ZoneId] OffsetDateTime + (fn [^Instant i, ^ZoneId z] + (OffsetDateTime/ofInstant i z))) + +(conversion! [Instant ZoneId] OffsetTime + (fn [^Instant i, ^ZoneId z] + (OffsetTime/ofInstant i z))) + +(conversion! [java.time.format.DateTimeFormatter CharSequence] ZonedDateTime + #(ZonedDateTime/from (jt.f/parse %1 %2))) + +(conversion! [java.time.format.DateTimeFormatter CharSequence] OffsetDateTime + #(OffsetDateTime/from (jt.f/parse %1 %2))) + +(conversion! [java.time.format.DateTimeFormatter CharSequence] OffsetTime + #(OffsetTime/from (jt.f/parse %1 %2))) + +(conversion! [Number ZoneId] ZonedDateTime + (fn [value ^ZoneId z] + (zoned-date-time value 1 1 z))) + +(conversion! [Number ZoneOffset] OffsetDateTime + (fn [value ^ZoneOffset zo] + (offset-date-time value 1 1 zo))) + +(conversion! [Number ZoneOffset] OffsetTime + (fn [value ^ZoneOffset zo] + (offset-time value 0 zo))) + +(conversion! [Number Number ZoneId] ZonedDateTime + (fn [y m ^ZoneId z] + (zoned-date-time y m 1 z))) + +(conversion! [Number Number ZoneOffset] OffsetDateTime + (fn [y m ^ZoneOffset zo] + (offset-date-time y m 1 zo))) + +(conversion! [Number Number ZoneOffset] OffsetTime + (fn [h m ^ZoneOffset zo] + (offset-time h m 0 zo))) + +(conversion! java.util.GregorianCalendar ZonedDateTime + (fn [^java.util.GregorianCalendar cal] + (.toZonedDateTime cal))) + +(extend-type OffsetDateTime + jt.c/Truncatable + (truncate-to [o u] + (.truncatedTo o (get-unit-checked u))) + + jt.c/Ordered + (single-after? [d o] + (.isAfter d o)) + (single-before? [d o] + (.isBefore d o))) + +(extend-type OffsetTime + jt.c/Truncatable + (truncate-to [o u] + (.truncatedTo o (get-unit-checked u))) + + jt.c/Ordered + (single-after? [d o] + (.isAfter d o)) + (single-before? [d o] + (.isBefore d o))) + +(extend-type ZonedDateTime + jt.c/Truncatable + (truncate-to [o u] + (.truncatedTo o (get-unit-checked u)))) + +;;;;; Clock + +(defn ^Clock system-clock + "Creates a system clock. In the default timezone if called without arguments, + otherwise accepts a Zone Id." + ([] (Clock/systemDefaultZone)) + ([k] (Clock/system (zone-id k)))) + +(defn ^Clock fixed-clock + "Creates a fixed clock either at the current instant or at the supplied + instant/instant + zone." + ([] (Clock/fixed (Instant/now) (zone-id))) + ([i] (Clock/fixed (jt.t/instant i) (zone-id))) + ([i z] (Clock/fixed (jt.t/instant i) (zone-id z)))) + +(defn ^Clock offset-clock + "Creates a clock offset from the current/provided clock by a given + `duration`." + ([d] (Clock/offset (system-clock) (jt.a/duration d))) + ([^Clock c, d] (Clock/offset c (jt.a/duration d)))) + +(defn ^Clock tick-clock + "Creates a clock wrapping system/provided clock that only ticks as per + specified duration." + ([d] (Clock/tick (system-clock) (jt.a/duration d))) + ([^Clock c, d] (Clock/tick c (jt.a/duration d)))) + +(extend-type Clock + jt.c/ReadableProperty + (value [c] (.millis c)) + + jt.c/HasZone + (with-zone [c z] + (.withZone c (zone-id z))) + + jt.c/Ordered + (single-after? [c o] + (> (.millis c) (.millis ^Clock o))) + (single-before? [c o] + (< (.millis c) (.millis ^Clock o)))) + +;; Avoid cyclic dep +(extend-type java.time.format.DateTimeFormatter + jt.c/HasZone + (with-zone [dtf zone] + (.withZone dtf (zone-id zone)))) diff --git a/test/java_time/graph_test.clj b/test/java_time/graph_test.clj new file mode 100644 index 0000000..bc78a1b --- /dev/null +++ b/test/java_time/graph_test.clj @@ -0,0 +1,109 @@ +(ns java-time.graph-test + (:require [java-time.graph :as sut] + [clojure.test :as t :refer :all])) + +(deftest types + (testing "identical" + (is (= (sut/types [Object]) (sut/types [Object]))) + (is (= (sut/types [Object String]) (sut/types [Object String])))) + + (testing "assignable" + (testing "single arity" + (is (sut/assignable? (sut/types [Object]) (sut/types [Object]))) + (is (sut/assignable? (sut/types [String]) (sut/types [Object]))) + (is (not (sut/assignable? (sut/types [Object]) (sut/types [String])))) + (is (not (sut/assignable? (sut/types [String]) (sut/types [Number]))))) + + (testing "multi arity" + (is (sut/assignable? (sut/types [Object Number]) (sut/types [Object Object]))) + (is (not (sut/assignable? (sut/types [Object Number]) (sut/types [Object String])))) + (is (not (sut/assignable? (sut/types [Object Object]) (sut/types [Object])))) + (is (not (sut/assignable? (sut/types [Object]) (sut/types [Object Object]))))))) + +(defn mk-graph [& conversions] + (loop [g (sut/conversion-graph) + [[in out f cost] & conversions] conversions] + (let [g (sut/assoc-conversion g (sut/types in) (sut/types out) f (or cost 1))] + (if (seq conversions) + (recur g conversions) + g)))) + +(defn conversion-fn [g in out] + (sut/conversion-fn g (sut/types in) (sut/types out))) + +(deftest empty-graph + (let [graph (sut/conversion-graph) + t (sut/types [Object])] + (is (empty? (sut/equivalent-targets graph t))) + (is (empty? (sut/possible-conversions graph t))))) + +(deftest single-arity-conversions + (testing "implicit" + (is (= [11] ((second (sut/conversion-fn (sut/conversion-graph) + (sut/types [Number]) + (sut/types [Number]))) [11])))) + + (testing "success" + (let [g (mk-graph [[String] [Number] (fn [v] [(Integer/parseInt (first v))])])] + (is (= [11] ((second (conversion-fn g [String] [Number])) ["11"]))))) + + (testing "failure" + (let [g (mk-graph [[String] [Number] (fn [v] [(Integer/parseInt (first v))])])] + (is (nil? (conversion-fn g [Object] [Number]))))) + + (testing "chain" + (let [g (mk-graph + [[Number] [String] (comp vector str first)] + [[String] [clojure.lang.Keyword] (comp vector keyword first)])] + (is (= [:11] ((second (conversion-fn g [Number] [clojure.lang.Keyword])) [11])))))) + +(deftest multi-arity-conversions + (testing "implicit" + (is (= [1 1] ((second (sut/conversion-fn (sut/conversion-graph) + (sut/types [Number Number]) + (sut/types [Number Number]))) [1 1])))) + + (testing "simple" + (let [g (mk-graph [[String String] [String] (fn [[a b]] [(str a "," b)])])] + (is (= ["a,b"] ((second (conversion-fn g [String String] [String])) ["a" "b"]))))) + + (testing "chain" + (let [g (mk-graph + [[Number Number] [String] (fn [[a b]] [(str (+ a b))])] + [[String] [clojure.lang.Keyword] (comp vector keyword first)])] + (is (= [:9] ((second (conversion-fn g [Number Number] [clojure.lang.Keyword])) [4 5]))))) + + (testing "same arity" + (let [g (mk-graph [[Number] [String] (comp vector str first)])] + (is (= ["4" "5"] ((second (conversion-fn g [Number Number] [String String])) [4 5]))))) + + (testing "same arity chain" + (let [g (mk-graph [[Number] [String] (comp vector str first)] + [[String] [clojure.lang.Keyword] (comp vector keyword first)])] + (is (= ["4" "x" :5] ((second (conversion-fn g [Number String Number] [String String clojure.lang.Keyword])) + [4 "x" 5]))))) + + (testing "different arity" + (let [g (mk-graph + [[Number] [String] (fn [x] (vector (str "number-" (first x))))] + [[String String] [clojure.lang.Keyword] (fn [[n v]] (vector (keyword n v)))])] + (is (= [:number-4/hi] ((second (conversion-fn g [Number String] [clojure.lang.Keyword])) [4 "hi"])))))) + +(deftest multi-arity-multi-step + (testing "multiple steps" + (with-redefs [sut/max-arity 4 + sut/max-extent 3 + sut/max-path-length 6] + (let [g (mk-graph + [[Number] [String] (fn [x] (vector (str "number-" (first x))))] + [[String String] [clojure.lang.Keyword] (fn [[n v]] [(keyword n v)])] + [[clojure.lang.Keyword clojure.lang.Keyword] [String] (fn [[k1 k2]] [(str k1 "!" k2)])])] + (is (= [":number-4/hi!:number-5/ho"] + ((second (conversion-fn g [Number String Number String] [String])) [4 "hi" 5 "ho"]))))))) + +(deftest non-empty-graph + (let [g (mk-graph [[String] [Number] (fn [v] (Integer/parseInt v))] + [[String] [Long] (fn [v] (Long/parseLong v))] + [[clojure.lang.Keyword] [String] str])] + (is (= #{(sut/types [Number]) (sut/types [Long])} (set (sut/equivalent-targets g (sut/types [Number]))))) + (is (= 2 (count (sut/possible-conversions g (sut/types [String]))))))) diff --git a/test/java_time_test.clj b/test/java_time_test.clj new file mode 100644 index 0000000..21c64f4 --- /dev/null +++ b/test/java_time_test.clj @@ -0,0 +1,727 @@ +(ns java-time-test + (:require [clojure.test :refer :all] + [java-time.util :as jt.u] + [java-time :as j])) + +(def clock (j/fixed-clock "2015-11-26T10:20:30.000000040Z" "UTC")) + +(deftest constructors + (testing "clocks" + (testing ", with-clock" + (are [f] (= (j/with-clock clock (f)) (f clock)) + j/zoned-date-time + j/offset-date-time + j/offset-time + j/local-date-time + j/local-time + j/local-date + j/zone-offset + j/zone-id)) + + (testing ", system" + (let [now-millis (j/value (j/system-clock))] + (is (<= now-millis + (j/value (j/system-clock "UTC")))) + (is (= (j/system-clock "UTC") + (j/with-zone (j/system-clock "Europe/Zurich") "UTC"))))) + + (testing ", fixed" + (is (= (j/value (j/fixed-clock "2015-01-01T10:20:30Z" "UTC")) + (j/value (j/fixed-clock "2015-01-01T10:20:30Z"))))) + + (testing ", offset" + (is (= (str (-> (j/fixed-clock "2015-01-01T10:20:30Z" "UTC") + (j/offset-clock (j/minutes 30)))) + "OffsetClock[FixedClock[2015-01-01T10:20:30Z,UTC],PT30M]"))) + + (testing ", tick" + (is (= (str (-> (j/fixed-clock "2015-01-01T10:20:30Z" "UTC") + (j/tick-clock (j/minutes 10)))) + "TickClock[FixedClock[2015-01-01T10:20:30Z,UTC],PT10M]")))) + + (testing "offsets" + (is (= (j/zone-offset +0) + (j/zone-offset "+00:00") + (j/zone-offset -0) + (j/zone-offset 0 0))) + + (is (= (j/zone-offset 1 30) + (j/zone-offset "+01:30") + (j/zone-offset 1 30 0) + (j/zone-offset +1.5)))) + + (testing "enums" + (is (= (j/month 11) + (j/month :november) + (j/month (j/local-date clock)) + (j/month "MM" "11"))) + + (is (j/month? (j/month 7))) + + (is (= (j/day-of-week 4) + (j/day-of-week :thursday) + (j/day-of-week (j/local-date clock)) + (j/day-of-week "ee" "05"))) + + (is (j/day-of-week? (j/day-of-week 4)))) + + (testing "multi field" + (is (= (j/month-day (j/local-date clock)) + (j/month-day 11 26) + (j/month-day "dd-MM" "26-11"))) + + (is (= (j/month-day 1) + (j/month-day (j/month 1)) + (j/month-day 1 1))) + + (is (j/month-day? (j/month-day 1 1))) + + (is (= (j/year-month (j/local-date clock)) + (j/year-month 2015 11) + (j/year-month "yy-MM" "15-11"))) + + (is (= (j/year-month 1) + (j/year-month (j/year 1)) + (j/year-month 1 1))) + + (is (j/year-month? (j/year-month 1 1)))) + + (testing "years" + (is (= (j/year clock) + (j/year "2015") + (j/year 2015) + (j/year "yy" "15"))) + + (is (= (j/year "UTC") + (j/year (j/zone-id "UTC")))) + + (is (j/year? (j/year 2015)))) + + (testing "local date" + (is (= (j/local-date clock) + (j/local-date 2015 11 26) + (j/local-date "2015-11-26") + (j/local-date "yyyy/MM/dd" "2015/11/26") + (j/local-date (j/local-date 2015 11 26)) + (j/local-date (j/local-date-time clock)) + (j/local-date (j/zoned-date-time clock)) + (j/local-date (j/offset-date-time clock)) + (j/local-date (j/instant clock) "UTC") + (j/local-date (j/to-java-date clock) "UTC"))) + + (is (j/local-date? (j/local-date))) + + (is (= (j/local-date 2015) + (j/local-date 2015 1) + (j/local-date 2015 1 1) + (j/local-date (j/year 2015) (j/month 1)) + (j/local-date (j/year 2015) (j/month 1) (j/day-of-week 1))))) + + (testing "local time" + (is (= (j/local-time clock) + (j/local-time 10 20 30 40) + (j/local-time "10:20:30.000000040") + (j/local-time "HH:mm,ss:SSSSSSSSS" "10:20,30:000000040") + (j/local-time (j/local-time clock)) + (j/local-time (j/local-date-time clock)) + (j/local-time (j/zoned-date-time clock)) + (j/local-time (j/offset-date-time clock)) + (j/local-time (j/instant clock) "UTC"))) + + (is (= (j/truncate-to (j/local-time clock) :millis) + (j/local-time (j/to-java-date clock) "UTC"))) + + (is (j/local-time? (j/local-time))) + + (is (= (j/local-time 10) + (j/local-time 10 0) + (j/local-time 10 0 0) + (j/local-time 10 0 0 0))) + + (is (= (j/truncate-to (j/local-time 10 20 30 40) :minutes) + (j/local-time 10 20)))) + + (testing "local date time" + (is (= (j/local-date-time clock) + (j/local-date-time 2015 11 26 10 20 30 40) + (j/local-date-time "2015-11-26T10:20:30.000000040") + (j/local-date-time "yyyy/MM/dd'T'SSSSSSSSS,HH:mm:ss" "2015/11/26T000000040,10:20:30") + (j/local-date-time (j/local-date 2015 11 26) (j/local-time 10 20 30 40)) + (j/local-date-time (j/local-date-time clock)) + (j/local-date-time (j/zoned-date-time clock)) + (j/local-date-time (j/offset-date-time clock)) + (j/local-date-time (j/instant clock) "UTC"))) + + (is (= (j/truncate-to (j/local-date-time clock) :millis) + (j/local-date-time (j/to-java-date clock) "UTC"))) + + (is (j/local-date-time? (j/local-date-time))) + + (is (= (j/local-date-time 2015) + (j/local-date-time 2015 1) + (j/local-date-time 2015 1 1) + (j/local-date-time (j/year 2015) (j/month 1)) + (j/local-date-time (j/year 2015) (j/month 1) (j/day-of-week 1)) + (j/local-date-time 2015 1 1 0) + (j/local-date-time 2015 1 1 0 0) + (j/local-date-time 2015 1 1 0 0 0) + (j/local-date-time 2015 1 1 0 0 0 0))) + + (is (= (j/truncate-to (j/local-date-time 2015 1 1 10 20 30 40) :minutes) + (j/local-date-time 2015 1 1 10 20)))) + + (testing "zoned date time" + (is (= (j/zoned-date-time clock) + (j/zoned-date-time (j/zoned-date-time clock)) + (j/zoned-date-time "2015-11-26T10:20:30.000000040+00:00[UTC]") + (j/zoned-date-time "2015-11-26T10:20:30.000000040Z[UTC]") + (j/zoned-date-time "yyyy/MM/dd'T'HH:mm:ss-SSSSSSSSS'['VV']'" "2015/11/26T10:20:30-000000040[UTC]") + (j/zoned-date-time (j/local-date clock) (j/local-time clock) "UTC") + (j/zoned-date-time (j/local-date-time clock) "UTC") + (j/zoned-date-time (j/offset-date-time clock) "UTC") + (j/zoned-date-time 2015 11 26 10 20 30 40 "UTC") + (j/zoned-date-time (j/instant clock) "UTC"))) + + (is (= (j/truncate-to (j/zoned-date-time clock) :millis) + (j/zoned-date-time (j/to-java-date clock) "UTC"))) + + (is (j/zoned-date-time? (j/zoned-date-time (j/zone-id "UTC")))) + + (is (= (j/zoned-date-time 2015 "UTC") + (j/zoned-date-time 2015 1 "UTC") + (j/zoned-date-time 2015 1 1 "UTC") + (j/zoned-date-time (j/year 2015) "UTC") + (j/zoned-date-time (j/year 2015) (j/month 1) "UTC") + (j/zoned-date-time (j/year 2015) (j/month 1) (j/day-of-week 1) "UTC") + (j/zoned-date-time 2015 1 1 0 "UTC") + (j/zoned-date-time 2015 1 1 0 0 "UTC") + (j/zoned-date-time 2015 1 1 0 0 0 "UTC") + (j/zoned-date-time 2015 1 1 0 0 0 0 "UTC"))) + + (is (= (j/truncate-to (j/zoned-date-time 2015 1 1 10 20 30 40 "UTC") :minutes) + (j/zoned-date-time 2015 1 1 10 20 "UTC")))) + + (testing "offset date time" + (is (= (j/offset-date-time clock) + (j/offset-date-time (j/offset-date-time clock)) + (j/offset-date-time "2015-11-26T10:20:30.000000040+00:00") + (j/offset-date-time "2015-11-26T10:20:30.000000040Z") + (j/offset-date-time "yyyy/MM/dd'T'HH:mm:ss-SSSSSSSSS'['X']'" "2015/11/26T10:20:30-000000040[Z]") + (j/offset-date-time (j/local-date clock) (j/local-time clock) +0) + (j/offset-date-time (j/local-date-time clock) +0) + (j/offset-date-time (j/zoned-date-time clock) +0) + (j/offset-date-time 2015 11 26 10 20 30 40 +0) + (j/offset-date-time (j/instant clock) "UTC"))) + + (is (= (j/truncate-to (j/offset-date-time clock) :millis) + (j/offset-date-time (j/to-java-date clock) "UTC"))) + + (is (j/offset-date-time? (j/offset-date-time))) + + (is (= (j/offset-date-time 2015 +0) + (j/offset-date-time 2015 1 +0) + (j/offset-date-time 2015 1 1 +0) + (j/offset-date-time (j/year 2015) +0) + (j/offset-date-time (j/year 2015) (j/month 1) +0) + (j/offset-date-time (j/year 2015) (j/month 1) (j/day-of-week 1) +0) + (j/offset-date-time 2015 1 1 0 +0) + (j/offset-date-time 2015 1 1 0 0 +0) + (j/offset-date-time 2015 1 1 0 0 0 +0) + (j/offset-date-time 2015 1 1 0 0 0 0 +0))) + + (is (= (j/truncate-to (j/offset-date-time 2015 1 1 10 20 30 40 0) :minutes) + (j/offset-date-time 2015 1 1 10 20 0)))) + + (testing "offset time" + (is (= (j/offset-time clock) + (j/offset-time (j/offset-time clock)) + (j/offset-time (j/zoned-date-time clock)) + (j/offset-time "10:20:30.000000040+00:00") + (j/offset-time "10:20:30.000000040Z") + (j/offset-time "HH:mm:ss-SSSSSSSSS'['X']'" "10:20:30-000000040[Z]") + (j/offset-time (j/local-time clock) +0) + (j/offset-time (j/instant clock) "UTC") + (j/offset-time 10 20 30 40 +0) + (j/offset-time 10 20 30 40 +0) + (j/offset-time (j/instant clock) "UTC"))) + + (is (= (j/truncate-to (j/offset-time clock) :millis) + (j/offset-time (j/to-java-date clock) "UTC"))) + + (is (j/offset-time? (j/offset-time (j/zone-id "UTC")))) + (is (j/offset-time? (j/offset-time +0))) + + (is (= (j/offset-time 0 +0) + (j/offset-time 0 0 +0) + (j/offset-time 0 0 0 +0) + (j/offset-time 0 0 0 0 +0))) + + (is (= (j/truncate-to (j/offset-time 10 20 30 40 0) :minutes) + (j/offset-time 10 20 0)))) + + (testing "instant" + (is (= (j/instant clock) + (j/instant "2015-11-26T10:20:30.000000040Z") + (j/instant "yyyy/MM/dd'T'HH:mm:ss-SSSSSSSSS'['X']'" "2015/11/26T10:20:30-000000040[Z]") + (j/instant 1448533230 40))) + + (is (= (j/truncate-to (j/instant clock) :millis) + ;; (.toEpochMilli instant) + (j/instant 1448533230000)))) + + (testing "duration" + (is (= (j/duration 100) + (j/duration (j/duration 100)) + (j/duration "PT0.1S") + (j/duration (j/local-time 0 0 0 0) (j/local-time 0 0 0 (* 100 1000 1000))) + (j/duration 100 :millis))) + + (is (j/duration? (j/duration)))) + + (testing "period" + (is (= (j/period 10 20 30) + (j/period "P10Y20M30D"))) + + (is (= (j/period 11 9) + (j/period (j/local-date 2001 1 1) (j/local-date 2012 10 1)))) + + (is (= (j/period) + (j/period 0) + (j/period 0 0) + (j/period 0 0 0) + (j/period 0 :years) + (j/period 0 :months) + (j/period 0 :days))) + + (is (j/period? (j/period)))) + + (testing "interval" + (is (= (j/interval "1970-01-01T00:00:00Z/1970-01-01T00:00:01Z") + (j/interval 0 1000) + (j/interval (j/offset-date-time 1970 1 1 +0) + (j/offset-date-time 1970 1 1 0 0 1 +0)))))) + +(deftest operations + (testing "duration" + (testing "plus" + (is (= (j/duration 100000001) + (j/plus (j/standard-days 1) (j/hours 3) (j/minutes 46) (j/seconds 40) (j/millis 1) (j/nanos 0)) + (j/plus (j/duration 1 :days) + (j/duration 3 :hours) + (j/duration 46 :minutes) + (j/duration 40 :seconds) + (j/duration 1 :millis) + (j/duration 0 :nanos))))) + + (testing "minus" + (is (= (j/duration "PT22H58M58.998999999S") + (j/minus (j/standard-days 1) (j/hours 1) (j/minutes 1) (j/seconds 1) (j/millis 1) (j/nanos 1))))) + + (testing "multiply" + (is (= (j/hours 2) + (j/multiply-by (j/hours 1) 2)))) + + (testing "number ops" + (is (j/zero? (j/duration 0))) + (is (= (j/duration 10) (j/abs (j/duration -10)))) + (is (= (j/duration -10) (j/negate (j/duration 10)))) + (is (j/negative? (j/duration -10))))) + + (testing "period" + (testing "plus" + (is (= (j/period 10 20 30) + (j/plus (j/years 10) (j/months 20) (j/days 30)) + (j/plus (j/period 10) + (j/period 0 20) + (j/period 0 0 30)) + (j/plus (j/period 10 :years) + (j/period 20 :months) + (j/period 30 :days))))) + + (testing "minus" + (is (= (j/period 0 0 0) + (j/minus (j/period 10 20 30) + (j/years 10) (j/months 20) (j/days 30))))) + + (testing "multiply" + (is (= (j/days 2) + (j/multiply-by (j/days 1) 2)))) + + (testing "number ops" + (is (j/zero? (j/period 0))) + (is (= (j/period -10 10) (j/negate (j/period 10 -10)))) + (is (j/negative? (j/period -10))) + (is (j/negative? (j/period -10 10))))) + + (testing "year" + (testing "plus" + (is (= (j/year 5) + (j/plus (j/year 2) (j/years 3))))) + + (testing "minus" + (is (= (j/year 0) + (j/minus (j/year 5) (j/years 5)))))) + + (testing "month" + (testing "plus" + (is (= (j/month :may) + (j/plus (j/month 2) 3) + (j/plus (j/month 2) (j/months 3))))) + + (testing "minus" + (is (= (j/month :january) + (j/minus (j/month 5) 4) + (j/minus (j/month 5) (j/months 4)))))) + + (testing "day of week" + (testing "plus" + (is (= (j/day-of-week :sunday) + (j/plus (j/day-of-week 1) 6) + (j/plus (j/day-of-week 1) (j/days 6))))) + + (testing "minus" + (is (= (j/day-of-week :monday) + (j/minus (j/day-of-week 6) 5) + (j/minus (j/day-of-week 6) (j/days 5)))))) + + (testing "interval" + (is (= (j/interval 5000 10000) + (j/move-end-by (j/interval 5000 6000) (j/seconds 4)) + (j/move-start-by (j/interval 0 10000) (j/seconds 5)) + (j/move-end-to (j/interval 5000 6000) 10000) + (j/move-start-to (j/interval 0 10000) 5000))) + + (is (= (j/instant 0) (j/start (j/interval 0 1000)))) + (is (= (j/instant 1000) (j/end (j/interval 0 1000)))) + + (testing "contains" + (is (j/contains? (j/interval 0 1000) 500)) + (is (not (j/contains? (j/interval 0 1000) 1500))) + (is (j/contains? (j/interval 0 1000) (j/interval 100 900))) + (is (j/contains? (j/interval 0 1000) (j/interval 0 900))) + (is (j/contains? (j/interval 0 1000) (j/interval 0 1000))) + (is (j/contains? (j/interval 0 1000) (j/interval 1000 1000))) + (is (not (j/contains? (j/interval 0 1000) (j/interval 1000 1001))))) + + (testing "overlaps" + (is (j/overlaps? (j/interval 0 1000) (j/interval 0 500))) + (is (j/overlaps? (j/interval 0 1000) (j/interval 0 1500))) + (is (j/overlaps? (j/interval 500 1000) (j/interval 0 1500))) + (is (not (j/overlaps? (j/interval 0 1000) (j/interval 1500 2000)))) + + (is (= (j/interval 500 1000) (j/overlap (j/interval 500 1000) (j/interval 0 1500)))) + (is (nil? (j/overlap (j/interval 0 1000) (j/interval 1500 2000))))) + + (testing "abuts" + (is (j/abuts? (j/interval 0 1000) (j/interval 1000 2000))) + (is (not (j/abuts? (j/interval 0 1000) (j/interval 900 2000))))) + + (testing "gap" + (is (= (j/interval 1000 2000) (j/gap (j/interval 0 1000) (j/interval 2000 3000)))) + (is (nil? (j/gap (j/interval 0 1000) (j/interval 500 1500))))))) + +(deftest ordering + (testing "interval" + (is (j/before? (j/interval 1000 2000) (j/instant 5000))) + (is (not (j/before? (j/interval 1000 5000) (j/instant 5000)))) + (is (j/before? (j/interval 1000 5000) (j/interval 5001 6000))) + + (is (j/after? (j/interval 1000 5000) (j/instant 100))) + (is (not (j/after? (j/interval 1000 5000) (j/instant 2000)))) + (is (j/after? (j/interval 1000 5000) (j/interval 100 999)))) + + (testing "times" + (is (j/after? (j/local-date-time clock) (j/minus (j/local-date-time clock) (j/days 5)))) + (is (j/before? (j/local-date-time clock) (j/plus (j/local-date-time clock) (j/days 5)))) + + (is (j/after? (j/local-date clock) (j/minus (j/local-date clock) (j/days 5)))) + (is (j/before? (j/local-date clock) (j/plus (j/local-date clock) (j/days 5)))) + + (is (j/after? (j/local-time clock) (j/minus (j/local-time clock) (j/minutes 5)))) + (is (j/before? (j/local-time clock) (j/plus (j/local-time clock) (j/minutes 5)))) + + (is (j/after? (j/zoned-date-time clock) (j/minus (j/zoned-date-time clock) (j/minutes 5)))) + (is (j/before? (j/zoned-date-time clock) (j/plus (j/zoned-date-time clock) (j/minutes 5)))) + + (is (j/after? (j/offset-date-time clock) (j/minus (j/offset-date-time clock) (j/minutes 5)))) + (is (j/before? (j/offset-date-time clock) (j/plus (j/offset-date-time clock) (j/minutes 5)))) + + (is (j/after? (j/offset-time clock) (j/minus (j/offset-time clock) (j/minutes 5)))) + (is (j/before? (j/offset-time clock) (j/plus (j/offset-time clock) (j/minutes 5))))) + + (testing "clocks" + (is (j/after? (j/fixed-clock 1000) (j/fixed-clock 0))) + (is (j/before? (j/fixed-clock 1000) (j/fixed-clock 5000)))) + + (testing "fields" + (is (j/after? (j/day-of-week :saturday) :thursday)) + (is (j/before? (j/day-of-week :saturday) :sunday)) + + (is (j/after? (j/month :february) :january)) + (is (j/before? (j/month :february) :march)) + + (is (j/after? (j/year 2010) 2009)) + (is (j/before? (j/year 2010) 2011)) + + (is (j/after? (j/month-day 5 1) (j/month-day 4 1))) + (is (j/before? (j/month-day 1 1) (j/month-day 4 1))))) + +(deftest properties + (testing "units" + (is (= (j/unit :seconds) + (j/unit (j/duration) :seconds) + (j/unit (j/duration) (j/unit :seconds)))) + + (is (j/unit? (j/unit :seconds))) + + (is (j/supports? (j/duration) :seconds)) + (is (j/supports? :seconds (j/local-date-time))) + (is (not (j/supports? :seconds (j/local-date)))) + + (is (= 60 + (j/time-between (j/local-time "15:40") (j/local-time "15:41") :seconds) + (j/time-between :seconds (j/local-time "15:40") (j/local-time "15:41")) + (j/time-between (j/unit :seconds) (j/local-time "15:40") (j/local-time "15:41"))))) + + (testing "fields" + (is (= (j/field :second-of-day) + (j/field (j/local-date-time) :second-of-day) + (j/field (j/local-date-time) (j/field :second-of-day)))) + + (is (j/field? (j/field :second-of-day))) + + (is (j/supports? (j/local-date-time) :second-of-day)) + (is (j/supports? :second-of-day (j/local-date-time))) + (is (j/supports? :rata-die (j/local-date-time))) + (is (not (j/supports? :second-of-day (j/local-date)))) + + (testing ", ranges" + (is (= (j/value-range 0 86399) + (j/range :second-of-day))) + + (is (= (j/value-range {:min-smallest 1, :min-largest 1, :max-smallest 28, :max-largest 31}) + (j/range :day-of-month) + (j/range (j/field :day-of-month)))) + + (is (= 1 (j/min-value :day-of-month))) + (is (= 1 (j/largest-min-value :day-of-month))) + (is (= 28 (j/smallest-max-value :day-of-month))) + (is (= 31 (j/max-value :day-of-month))))) + + (testing "duration" + (let [d (j/duration 100000001)] + (is (= (j/properties d) + {:nanos (j/property d :nanos) + :seconds (j/property d :seconds)})) + (is (= (j/units d) + {:nanos (j/unit :nanos) + :seconds (j/unit :seconds)})))) + + (testing "period" + (let [p (j/period 10 5 1)] + (is (= (j/properties p) + {:days (j/property p :days) + :months (j/property p :months) + :years (j/property p :years)})) + (is (= (j/units p) + {:days (j/unit :days) + :months (j/unit :months) + :years (j/unit :years)})))) + + (testing "temporals" + (doseq [e [(j/local-date) + (j/local-time) + (j/local-date-time) + (j/offset-date-time) + (j/offset-time) + (j/zoned-date-time)]] + (is (seq (j/properties e))) + (is (seq (j/fields e))))) + + (testing "single fields" + (doseq [e [(j/month :february) + (j/day-of-week :monday) + (j/year 100) + (j/zone-offset 5 20)]] + (is (seq (j/properties e))) + (is (seq (j/fields e))) + (is (j/range e)))) + + (testing "multi fields" + (doseq [e [(j/month-day :january 1) + (j/year-month 10 10)]] + (is (seq (j/properties e))) + (is (seq (j/fields e)))))) + +(deftest seq-test + (is (= [(j/local-date 2015) (j/local-date 2016)] + (take 2 (j/iterate j/plus (j/local-date 2015) (j/years 1)))))) + +(deftest adjuster-test + (testing "predefined adjusters" + (is (= (j/adjust (j/local-date 2015 1 1) :next-working-day) + (j/local-date 2015 1 2))) + + (is (= (j/adjust (j/local-date 2015 1 1) :first-in-month :monday) + (j/local-date 2015 1 5))) + + (is (= (j/adjust (j/local-date 2015 1 1) :day-of-week-in-month 1 :monday) + (j/local-date 2015 1 5))) + + (is (= (j/adjust (j/local-date 2015 1 1) :day-of-week-in-month 2 :monday) + (j/local-date 2015 1 12)))) + + (testing "functions as adjusters" + (is (= (j/adjust (j/local-date 2015 1 1) j/plus (j/days 1)) + (j/local-date 2015 1 2))))) + +(deftest sugar-test + (testing "weekdays" + (is (j/monday? (j/local-date 2015 1 5))) + (is (j/tuesday? (j/offset-date-time 2015 1 6 0))) + (is (j/wednesday? (j/zoned-date-time 2015 1 7 "UTC"))) + (is (j/thursday? (j/local-date-time 2015 1 8))) + (is (j/friday? (j/day-of-week 5))) + (is (j/saturday? (j/day-of-week :saturday))) + (is (j/sunday? (j/day-of-week 7)))) + + (testing "predicates" + (is (j/weekday? (j/local-date 2015 1 5))) + (is (not (j/weekday? (j/local-date 2015 1 4)))) + (is (j/weekend? (j/local-date 2015 1 4))) + (is (not (j/weekend? (j/local-date 2015 1 5)))))) + +(deftest convert-test + (testing "amount" + (is (= {:remainder 10, :whole 0} + (j/convert-amount 10 :seconds :minutes))) + (is (= {:remainder 323200, :whole 16} + (j/convert-amount 10000000 :seconds :weeks))) + (is (thrown? Exception + (j/convert-amount 10 :seconds :years))) + (is (thrown? Exception + (j/convert-amount 10 :years :forever)))) + + (testing "as" + (testing "duration" + (is (= 0 (j/as (j/duration 10 :seconds) :minutes))) + (is (= 10 (j/as (j/duration 10 :seconds) :seconds))) + (is (= 10000 (j/as (j/duration 10 :seconds) :millis))) + (is (thrown? Exception (j/as (j/duration 10 :seconds) :months)))) + + (testing "period" + (is (= 0 (j/as (j/days 1) :weeks))) + (is (= (* 24 60) (j/as (j/days 1) :minutes))) + (is (thrown? Exception (j/as (j/months 1) :minutes))) + (is (thrown? Exception (j/as (j/period 1 1 1) :months))) + (is (= 13 (j/as (j/period 1 1) :months)))) + + (testing "temporal" + (is (= 1 (j/as (j/local-date 2015 1 1) :day-of-month))) + (is (= 2015 (j/as (j/local-date 2015 1 1) :year)))) + + (testing "multiple" + (is (= [2015 1 1] (j/as (j/local-date 2015 1 1) :year :month-of-year :day-of-month)))) + + (testing "throws" + (is (thrown? Exception (j/as (j/local-time 0) :year)))) + + (testing "interval" + (is (= 1 (j/as (j/interval (j/instant 0) (j/instant 1)) :millis)))))) + +(deftest legacy-conversion + (testing "converts through instant" + (is (= (j/instant 1000) (j/instant (java.util.Date. 1000)))) + (is (= (java.util.Date. 1000) (j/to-java-date 1000))) + (is (= (java.sql.Date. 1000) (j/to-sql-date 1000))) + (is (= (java.sql.Timestamp. 1000) (j/to-sql-timestamp 1000))) + (is (= 1000 + (j/to-millis-from-epoch 1000) + (j/to-millis-from-epoch (java.util.Date. 1000)) + (j/to-millis-from-epoch (j/offset-date-time (j/instant 1000) +0))))) + + (testing "from java.util Date types" + (is (= (j/zone-id "UTC") (j/zone-id (java.util.TimeZone/getTimeZone "UTC")))))) + +(jt.u/when-joda + + (def joda-clock (j/fixed-clock "2015-11-26T10:20:30.040Z" "UTC")) + + (import '[org.joda.time Duration Period DateTimeZone + LocalDate LocalTime LocalDateTime DateTime Instant]) + (deftest joda + (testing "duration from duration and period" + (is (= (j/duration 1 :millis) + (j/duration (Duration/millis 1)) + (j/duration (Period/millis 1)))) + (is (= (j/duration 1 :seconds) + (j/duration (Duration/standardSeconds 1)) + (j/duration (Period/seconds 1)))) + (is (= (j/duration 1 :minutes) + (j/duration (Duration/standardMinutes 1)) + (j/duration (Period/minutes 1)))) + (is (= (j/duration 1 :hours) + (j/duration (Duration/standardHours 1)) + (j/duration (Period/hours 1)))) + (is (= (j/duration 1 :days) + (j/duration (Duration/standardDays 1)) + (j/duration (Period/days 1)))) + + (is (= (j/plus (j/millis 1) (j/seconds 1) (j/minutes 1) (j/hours 1) + (j/standard-days 1)) + (j/duration (.plus (Duration/millis 1) + (.plus (Duration/standardSeconds 1) + (.plus (Duration/standardMinutes 1) + (.plus (Duration/standardHours 1) + (Duration/standardDays 1))))))))) + + (testing "duration from period" + (is (= (j/duration 7 :days) (j/duration (Period/weeks 1))))) + + (testing "period from duration" + (is (= (j/period 1 :days) (j/period (Duration/standardHours 24))))) + + (testing "period from joda period" + (is (= (j/period 1 :days) (j/period (Period/days 1)))) + (is (= (j/period 7 :days) (j/period (Period/weeks 1)))) + (is (= (j/period 1 :months) (j/period (Period/months 1)))) + (is (= (j/period 1 :years) (j/period (Period/years 1)))) + + (is (= (j/plus (j/days 1) (j/months 1) (j/years 1)) + (j/period (.plus (Period/days 1) + (.plus (Period/months 1) + (Period/years 1))))))) + + #_(testing "instant" + (is (= (j/instant joda-clock) + (j/instant (Instant. (DateTime. 2015 11 26 10 20 30)))))) + + (testing "local date" + (is (= (j/local-date joda-clock) + (j/local-date (LocalDate. 2015 11 26)) + (j/local-date (LocalDateTime. 2015 11 26 10 20 30)) + (j/local-date (DateTime. 2015 11 26 10 20 30))))) + + (testing "local date-time" + (is (= (j/local-date-time joda-clock) + (j/local-date-time (LocalDateTime. 2015 11 26 10 20 30 40)) + (j/local-date-time (DateTime. 2015 11 26 10 20 30 40))))) + + (testing "local time" + (is (= (j/local-time joda-clock) + (j/local-time (LocalTime. 10 20 30 40)) + (j/local-time (LocalDateTime. 2015 11 26 10 20 30 40)) + (j/local-time (DateTime. 2015 11 26 10 20 30 40))))) + + (testing "zoned date-time" + (is (= (j/zoned-date-time joda-clock) + (j/zoned-date-time (DateTime. 2015 11 26 10 20 30 40 (DateTimeZone/forID "UTC")))))) + + (testing "offset date-time" + (is (= (j/offset-date-time joda-clock) + (j/offset-date-time (DateTime. 2015 11 26 10 20 30 40 (DateTimeZone/forID "UTC")))))) + + (testing "offset time" + (is (= (j/offset-time joda-clock) + (j/offset-time (DateTime. 2015 11 26 10 20 30 40 (DateTimeZone/forID "UTC")))))))) +