├── bin
├── test
└── repl
├── circle.yml
├── test
├── gb.ics
├── cljs-test-opts.edn
└── tick
│ ├── calendar_test.clj
│ ├── addon_libs_test.cljc
│ ├── deprecated
│ ├── schedule_test.clj
│ ├── timeline_test.clj
│ └── cal_test.clj
│ ├── core_test.cljc
│ ├── ical_test.clj
│ └── alpha
│ └── api
│ └── dates_test.cljc
├── src
├── tick
│ ├── timezone.cljc
│ ├── locale_en_us.cljc
│ ├── file.clj
│ ├── time_literals.clj
│ ├── deprecated
│ │ ├── clock.clj
│ │ ├── timeline.clj
│ │ ├── cal.clj
│ │ └── schedule.clj
│ ├── calendar.clj
│ ├── format.cljc
│ ├── alpha
│ │ └── api.cljc
│ └── ical.clj
└── deps.cljs
├── docs
├── calendars.adoc
├── schedules.adoc
├── src
│ └── tick
│ │ └── docs
│ │ ├── app.clj
│ │ └── app.cljs
├── bibliography.adoc
├── cookbook
│ ├── index.adoc
│ ├── inst.adoc
│ ├── zone.adoc
│ ├── misc.adoc
│ ├── intervals.adoc
│ ├── arithmetick.adoc
│ ├── countdown.adoc
│ └── time.adoc
├── api.adoc
├── docinfo-footer.html
├── index.adoc
├── formatting.adoc
├── setup.adoc
├── durations.adoc
├── logo.svg
├── logo-normal.svg
├── clocks.adoc
├── intervals.adoc
├── cljs.adoc
└── intro.adoc
├── .nrepl.edn
├── tick.cljs.edn
├── package.json
├── dev
├── resources
│ ├── Screenshot-2017-10-30 Online Image Photo Editor - Shutterstock Editor.png
│ ├── clock-34354.svg
│ ├── mycal.svg
│ ├── calendar-33104.svg
│ ├── stopwatch-25763.svg
│ └── calendar-152134.svg
└── src
│ ├── cljs.clj
│ ├── user.clj
│ └── tick
│ └── viz.clj
├── docs.cljs.edn
├── .gitignore
├── aliases
├── rebel
│ └── tick
│ │ └── rebel
│ │ └── main.clj
└── nrepl
│ └── nrepl.clj
├── LICENSE
├── .circleci
└── config.yml
├── Makefile
├── pom.xml
├── README.adoc
├── deps.edn
├── generate_docs
└── resources
└── ics
└── gov.uk
├── england-and-wales.ics
└── scotland.ics
/bin/test:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | clojure -A:test $*
4 |
--------------------------------------------------------------------------------
/circle.yml:
--------------------------------------------------------------------------------
1 | test:
2 | override:
3 | - 'make test'
4 |
--------------------------------------------------------------------------------
/test/gb.ics:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gs/tick/master/test/gb.ics
--------------------------------------------------------------------------------
/src/tick/timezone.cljc:
--------------------------------------------------------------------------------
1 | (ns tick.timezone
2 | #?(:cljs (:require ["@js-joda/timezone"])))
3 |
--------------------------------------------------------------------------------
/src/deps.cljs:
--------------------------------------------------------------------------------
1 | {:npm-deps {"@js-joda/timezone" "2.2.0"
2 | "@js-joda/locale_en-us" "3.1.1"}}
--------------------------------------------------------------------------------
/docs/calendars.adoc:
--------------------------------------------------------------------------------
1 | = Calendars
2 |
3 | == Construction
4 |
5 | == Derivation
6 |
7 | == Comparison
8 |
--------------------------------------------------------------------------------
/docs/schedules.adoc:
--------------------------------------------------------------------------------
1 | = Schedules
2 |
3 | == Construction
4 |
5 | == Derivation
6 |
7 | == Comparison
8 |
--------------------------------------------------------------------------------
/.nrepl.edn:
--------------------------------------------------------------------------------
1 | { ;:bind "::"
2 | ;:transport nrepl.transport/tty
3 | :middleware [cider.piggieback/wrap-cljs-repl]}
--------------------------------------------------------------------------------
/bin/repl:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | echo "[Edge] Starting development environment, please wait..."
4 |
5 | clojure -A:dev:dev/nrepl:dev/rebel
6 |
--------------------------------------------------------------------------------
/tick.cljs.edn:
--------------------------------------------------------------------------------
1 | ^{:auto-testing true
2 | :open-url "http://localhost:9500"
3 | :watch-dirs ["src" "test"]}
4 | {:main tick.alpha.api}
--------------------------------------------------------------------------------
/test/cljs-test-opts.edn:
--------------------------------------------------------------------------------
1 | {:optimizations :advanced
2 | :cache-analysis true
3 | :pseudo-names true
4 | :infer-externs false
5 | :pretty-print true}
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tick",
3 | "version": "1.0.0",
4 | "private": true,
5 | "dependencies": {
6 | "@cljs-oss/module-deps": "^1.1.1"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/docs/src/tick/docs/app.clj:
--------------------------------------------------------------------------------
1 | (ns tick.docs.app
2 | (:require [cljs.env :as env]))
3 |
4 |
5 | (defmacro analyzer-state [[_ ns-sym]]
6 | `'~(get-in @env/*compiler* [:cljs.analyzer/namespaces ns-sym]))
7 |
--------------------------------------------------------------------------------
/dev/resources/Screenshot-2017-10-30 Online Image Photo Editor - Shutterstock Editor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gs/tick/master/dev/resources/Screenshot-2017-10-30 Online Image Photo Editor - Shutterstock Editor.png
--------------------------------------------------------------------------------
/docs/bibliography.adoc:
--------------------------------------------------------------------------------
1 | [bibliography]
2 | = References
3 |
4 | - [[[ISO8601]]] link:https://en.wikipedia.org/wiki/ISO_8601[ISO 8601: Data elements and interchange formats – Information interchange – Representation of dates and times]
5 |
--------------------------------------------------------------------------------
/docs.cljs.edn:
--------------------------------------------------------------------------------
1 | ^{:auto-testing false
2 | :open-url false
3 | :watch-dirs ["docs/src" ]}
4 | {:main tick.docs.app
5 | :output-to "docs/js/main.js"
6 | :verbose true
7 | :optimizations :simple
8 | ;:cache-analysis true
9 | }
--------------------------------------------------------------------------------
/src/tick/locale_en_us.cljc:
--------------------------------------------------------------------------------
1 | (ns tick.locale-en-us
2 | #?(:cljs (:require ["@js-joda/locale_en-us" :as js-joda-locale])))
3 |
4 | ; doing this for the one-arity tick.format/formatter. (npm users don't get js/JSJodaLocale global automatically)
5 | #?(:cljs (set! js/JSJodaLocale js-joda-locale))
6 |
7 |
8 |
--------------------------------------------------------------------------------
/test/tick/calendar_test.clj:
--------------------------------------------------------------------------------
1 | ;; Copyright © 2016-2018, JUXT LTD.
2 |
3 | (ns tick.calendar-test
4 | (:require
5 | [clojure.test :refer :all]
6 | [tick.calendar :as cal]))
7 |
8 | (deftest bank-holidays-in-england-and-wales-test
9 | (is (= 56 (count (cal/bank-holidays-in-england-and-wales)))))
10 |
--------------------------------------------------------------------------------
/src/tick/file.clj:
--------------------------------------------------------------------------------
1 | ;; Copyright © 2016-2017, JUXT LTD.
2 |
3 | (ns tick.file
4 | (:require
5 | [tick.core :as t])
6 | (:import
7 | [java.time Instant]))
8 |
9 | (extend-protocol t/IConversion
10 | java.io.File
11 | (instant [f] (Instant/ofEpochMilli (.lastModified f)))
12 | java.nio.file.Path
13 | (instant [f] (t/instant (.toFile f))))
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /classes
3 | /checkouts
4 | *.jar
5 | *.class
6 | /.lein-*
7 | /.nrepl-port
8 | .hgignore
9 | .hg/
10 | gh-pages
11 | /doc/index.html
12 | .cpcache/
13 | /DELETE_ME
14 | /.dir-locals.el
15 | /.shadow-cljs/
16 | /doc/js/
17 | /todo.org
18 | /project.clj.bak
19 | /bin/test-cljs
20 | node_modules/
21 | /docs/index.html
22 | /docs/js
23 | /cljs-test-runner-out
24 |
--------------------------------------------------------------------------------
/test/tick/addon_libs_test.cljc:
--------------------------------------------------------------------------------
1 | (ns tick.addon-libs-test
2 | (:require
3 | [tick.alpha.api :as t]
4 | [tick.timezone]
5 | [tick.locale-en-us]
6 | [tick.format :as tf]
7 | #?(:clj [clojure.test :refer :all]
8 | :cljs [cljs.test :refer-macros [deftest is testing run-tests]])))
9 |
10 | (deftest tz-test
11 | (is (t/zone "Europe/Berlin")))
12 |
13 | (deftest locale-test
14 | (is (tick.format/formatter "yyyy-MMM-dd")))
15 |
--------------------------------------------------------------------------------
/dev/src/cljs.clj:
--------------------------------------------------------------------------------
1 | (ns cljs
2 | (:require [figwheel.main.api :as fig]
3 | ))
4 |
5 |
6 | (defn figwheel-start! []
7 | (fig/start {:mode :serve} "tick")
8 | (println "auto run tests at http://localhost:9500/figwheel-extra-main/auto-testing"))
9 |
10 | (defn figwheel-stop! []
11 | (fig/stop-all))
12 |
13 | (defn cljs-repl []
14 | (fig/cljs-repl "tick"))
15 |
16 | (comment
17 |
18 | (figwheel-start!)
19 | (cljs-repl)
20 |
21 |
22 | )
--------------------------------------------------------------------------------
/docs/cookbook/index.adoc:
--------------------------------------------------------------------------------
1 | = Cookbook
2 |
3 | == Introduction
4 |
5 | This cookbook aims to give some examples of tick being used in different circumstances
6 | ranging from the very basic usage to more complex examples.
7 |
8 | include::time.adoc[]
9 |
10 | include::inst.adoc[]
11 |
12 | include::zone.adoc[]
13 |
14 | include::intervals.adoc[]
15 |
16 | include::arithmetick.adoc[]
17 |
18 | include::countdown.adoc[]
19 |
20 | include::misc.adoc[]
21 |
22 | include::reference.adoc[]
23 |
--------------------------------------------------------------------------------
/docs/api.adoc:
--------------------------------------------------------------------------------
1 | = API
2 |
3 | _Tick_ provides a single namespace—`tick.api`—containing the functions that make up the library's API.
4 |
5 | When you are using _tick_ in programs, it is a recommended idiom that you require _tick_'s api under the `t` alias as follows:
6 |
7 | ----
8 | (require '[tick.alpha.api :as t])
9 | ----
10 |
11 | CAUTION: Try to restrict your use of _tick_ to the `tick.api` namespace. Functions in other namespaces, which may not be marked private, are not part of the official API and could change.
12 |
--------------------------------------------------------------------------------
/docs/docinfo-footer.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
Copyright © 2018, JUXT LTD. Version: {revnumber}. Last modified on {revdate}.
7 |
8 |
9 |
14 |
15 |
--------------------------------------------------------------------------------
/dev/src/user.clj:
--------------------------------------------------------------------------------
1 | (ns user
2 | (:require
3 | [tick.alpha.api :as t]
4 | [tick.viz :refer [show-canvas view label]]
5 | [clojure.spec.alpha :as s]
6 | [clojure.tools.namespace.repl :refer [refresh refresh-all]]
7 | [cljs :refer :all]
8 | clojure.test))
9 |
10 | (set! *warn-on-reflection* true)
11 |
12 | (when (System/getProperty "nrepl.load")
13 | (require 'nrepl))
14 |
15 | (defn test-all []
16 | (refresh)
17 | (clojure.test/run-all-tests #"(tick).*test$"))
18 |
19 | (comment
20 | (refresh-all)
21 | (test-all)
22 |
23 | )
--------------------------------------------------------------------------------
/src/tick/time_literals.clj:
--------------------------------------------------------------------------------
1 | (ns tick.time-literals
2 | (:require [time-literals.read-write]
3 | [time-literals.data-readers]))
4 |
5 | (defonce
6 | ^{:dynamic true
7 | :doc "If true, include the time-literals printer, which will affect the way java.time and js-joda objects are printed"}
8 | *time-literals-printing*
9 | (not= "false" (System/getProperty "tick.time-literals.printing")))
10 |
11 | (defmacro modify-printing-of-time-literals-if-enabled! []
12 | (when *time-literals-printing*
13 | '(do
14 | (time-literals.read-write/print-time-literals-clj!)
15 | (time-literals.read-write/print-time-literals-cljs!))))
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/tick/deprecated/clock.clj:
--------------------------------------------------------------------------------
1 | ;; Copyright © 2016-2017, JUXT LTD.
2 |
3 | (ns tick.deprecated.clock
4 | (:import
5 | [java.time Clock ZoneId ZonedDateTime]
6 | [java.time.temporal ChronoUnit]))
7 |
8 | (defn clock-ticking-in-seconds []
9 | (Clock/tickSeconds (ZoneId/systemDefault)))
10 |
11 | (defn now
12 | ([] (ZonedDateTime/now))
13 | ([clock] (ZonedDateTime/now clock)))
14 |
15 | (defn just-now "Now, but truncated to the nearest second"
16 | ([] (.truncatedTo (now) (ChronoUnit/SECONDS)))
17 | ([clock] (.truncatedTo (now clock) (ChronoUnit/SECONDS))))
18 |
19 | (defn fixed-clock [^ZonedDateTime zdt]
20 | (Clock/fixed (.toInstant zdt) (.getZone zdt)))
21 |
--------------------------------------------------------------------------------
/aliases/rebel/tick/rebel/main.clj:
--------------------------------------------------------------------------------
1 | (ns tick.rebel.main
2 | (:require
3 | rebel-readline.clojure.main
4 | rebel-readline.core
5 | io.aviso.ansi))
6 |
7 | (defn -main
8 | [& args]
9 | (rebel-readline.core/ensure-terminal
10 | (rebel-readline.clojure.main/repl
11 | :init (fn []
12 | (try
13 | (println "[tick] Loading Clojure code, please wait...")
14 | (require 'user)
15 | (in-ns 'user)
16 | (catch Exception e
17 | (.printStackTrace e)
18 | (println "[tick] Failed to require user, this usually means there was a syntax error. See exception above.")))))))
19 |
--------------------------------------------------------------------------------
/docs/index.adoc:
--------------------------------------------------------------------------------
1 | = tick
2 | Malcolm Sparks ; Henry Widd; Johanna Antonelli
3 | 0.4.10-alpha, 2019-03-27
4 | :toc: left
5 | :toclevels: 4
6 | :docinfo: shared
7 | :sectnums: true
8 | :sectnumlevels: 2
9 | :xrefstyle: short
10 | :nofooter:
11 |
12 | :leveloffset: +1
13 |
14 | include::intro.adoc[]
15 |
16 | include::setup.adoc[]
17 |
18 | include::api.adoc[]
19 |
20 | include::dates.adoc[]
21 |
22 | include::durations.adoc[]
23 |
24 | include::clocks.adoc[]
25 |
26 | include::intervals.adoc[]
27 |
28 | include::calendars.adoc[]
29 |
30 | include::schedules.adoc[]
31 |
32 | include::formatting.adoc[]
33 |
34 | include::cookbook/index.adoc[]
35 |
36 | include::bibliography.adoc[]
37 |
--------------------------------------------------------------------------------
/docs/formatting.adoc:
--------------------------------------------------------------------------------
1 | = Formatting
2 |
3 | If it is de/serialization of java.time objects that is needed, then the https://clojars.org/time-literals[time-literals]
4 | library is the right tool for that.
5 |
6 | Tick includes a small formatting api over that provided by jsr-310
7 |
8 | In ClojureScript, require ns _[tick.locale-en-us]_ to create custom formatters
9 |
10 | ----
11 | (require '[tick.alpha.api :as t])
12 |
13 | (t/format :iso-zoned-date-time (tick/zoned-date-time))
14 |
15 | (require '[tick.locale-en-us]) ; only need this require for custom format patterns
16 | ; and it's only needed for cljs, although the ns is cljc
17 | (t/format (tick.format/formatter "yyyy-MMM-dd") (t/date))
18 | ----
19 |
20 |
--------------------------------------------------------------------------------
/docs/cookbook/inst.adoc:
--------------------------------------------------------------------------------
1 | == Instants and Inst
2 |
3 | tick's default convention is `java.time.Instant` but caters for projects that use
4 | `java.util.Date` by the conversions above. It is recommended when using tick to
5 | keep as an instant for as long as possible.
6 |
7 | === Creation
8 |
9 | ====
10 | To get the current instant:
11 |
12 | [source.code,clojure]
13 | ----
14 | (t/instant)
15 | ----
16 |
17 | [source.code,clojure]
18 | ----
19 | (t/now)
20 | ----
21 | ====
22 |
23 | ====
24 | Create a specific instant:
25 |
26 | [source.code,clojure]
27 | ----
28 | (t/instant "2000-01-01T00:00:00.001")
29 | ----
30 | ====
31 |
32 | === Conversions between Inst and Instant
33 |
34 | ====
35 | Convert inst to and from instant:
36 |
37 | [source.code,clojure]
38 | ----
39 | (t/instant (t/inst))
40 | ----
41 |
42 | [source.code,clojure]
43 | ----
44 | (t/inst (t/instant))
45 | ----
46 | ====
47 |
--------------------------------------------------------------------------------
/aliases/nrepl/nrepl.clj:
--------------------------------------------------------------------------------
1 | (ns ^{:clojure.tools.namespace.repl/load false} nrepl
2 | (:require
3 | [nrepl.server]
4 | [cider.piggieback]
5 | ;;[cider.nrepl]
6 | ;;[refactor-nrepl.middleware :as refactor.nrepl]
7 | [io.aviso.ansi]))
8 |
9 | (defn start-nrepl
10 | [opts]
11 | (let [server
12 | (nrepl.server/start-server
13 | :port (:port opts)
14 | :handler
15 | (nrepl.server/default-handler
16 | #_(conj (map #'cider.nrepl/resolve-or-fail cider.nrepl/cider-middleware)
17 | ;; #'refactor.nrepl/wrap-refactor
18 | )))]
19 | (spit ".nrepl-port" (:port server))
20 | (println (io.aviso.ansi/yellow (str "[tick] nREPL client can be connected to port " (:port server))))
21 | server))
22 |
23 | (println "[tick] Starting nREPL server")
24 |
25 | (def server (start-nrepl {:port 5610}))
26 |
--------------------------------------------------------------------------------
/docs/cookbook/zone.adoc:
--------------------------------------------------------------------------------
1 | == Time Zones & Offset
2 |
3 | ====
4 | Extract a zone from a `java.time.ZonedDateTime`:
5 |
6 | [source.code,clojure]
7 | ----
8 | (t/zone (t/zoned-date-time "2000-01-01T00:00:00Z[Europe/Paris]"))
9 | ----
10 |
11 | [source.code,clojure]
12 | ----
13 | (t/zone)
14 | ----
15 |
16 | ====
17 |
18 | ====
19 | Create a `java.time.ZonedDateTime` in a particular time zone:
20 |
21 | [source.code,clojure]
22 | ----
23 | (t/in (t/instant "2000-01-01T00:00") "Australia/Darwin")
24 | ----
25 | ====
26 |
27 | ====
28 | Give the `OffsetDateTime` instead of `ZonedDateTime`:
29 |
30 | [source.code,clojure]
31 | ----
32 | (t/offset-date-time (t/zoned-date-time "2000-01-01T00:00:00Z[Australia/Darwin]"))
33 | ----
34 | ====
35 |
36 | ====
37 | Specify the offset for a `LocalDateTime`:
38 |
39 | [source.code,clojure]
40 | ----
41 | (t/offset-by (t/date-time "2018-01-01T00:00") 9)
42 | ----
43 | ====
44 |
--------------------------------------------------------------------------------
/test/tick/deprecated/schedule_test.clj:
--------------------------------------------------------------------------------
1 | ;; Copyright © 2016-2017, JUXT LTD.
2 |
3 | (ns tick.deprecated.schedule-test
4 | (:require
5 | [clojure.test :refer :all]
6 | [tick.deprecated.timeline :refer [periodic-seq timeline]]
7 | [tick.deprecated.clock :refer [clock-ticking-in-seconds just-now]]
8 | [tick.deprecated.schedule :as sched]))
9 |
10 | #_(deftest ^:deprecated schedule-test
11 | (let [a (atom 0)
12 | f (fn [dt] (swap! a inc))
13 | clk (clock-ticking-in-seconds)
14 | now (just-now clk)
15 | timeline (take 10 (timeline (periodic-seq now (millis 10))))]
16 | @(sched/start (sched/schedule f timeline) clk)
17 | (is (= @a 10))))
18 |
19 | #_(deftest ^:deprecated simulate-test
20 | (let [a (atom 0)
21 | f (fn [dt] (swap! a inc))
22 | clk (clock-ticking-in-seconds)
23 | now (just-now clk)
24 | timeline (take 1000 (timeline (periodic-seq now (seconds 1))))]
25 | @(sched/start (sched/simulate f timeline) clk)
26 | (is (= @a 1000))))
27 |
--------------------------------------------------------------------------------
/test/tick/core_test.cljc:
--------------------------------------------------------------------------------
1 | ;; Copyright © 2016-2017, JUXT LTD.
2 |
3 | (ns tick.core-test
4 | (:require
5 | [clojure.spec.alpha :as s]
6 | [tick.core :as t]
7 | #?(:clj
8 | [clojure.test :refer :all]
9 | :cljs
10 | [cljs.test :refer-macros [deftest is testing run-tests]])
11 | #?(:cljs
12 | [java.time :refer [LocalDate Instant]]))
13 | #?(:clj (:import [java.time Instant LocalDate])))
14 |
15 | (s/check-asserts true)
16 |
17 | (deftest basics-test
18 | (is (instance? Instant (t/now)))
19 | (is (instance? LocalDate (t/today)))
20 | (is (instance? LocalDate (t/tomorrow)))
21 | (is (instance? LocalDate (t/yesterday))))
22 |
23 | (deftest divide-test
24 | (is
25 | ;; Duration -> Long -> Duration
26 | (= (t/new-duration 6 :hours) (t/divide (t/new-duration 6 :days) 24))
27 | ;; Duration -> Duration -> Long
28 | (= 63 (t/divide (t/new-duration 21 :days) (t/new-duration 8 :hours)))))
29 |
30 | (deftest construction-test
31 | (is (= (t/date "2018-01-11")
32 | (t/date (t/instant 1515691416624)))))
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright © 2016-2018 JUXT LTD.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/docs/setup.adoc:
--------------------------------------------------------------------------------
1 | = Setup
2 |
3 | Get the latest from https://clojars.org/tick[Clojars] and
4 | add to your `project.clj`, `build.boot` or `deps.edn`.
5 |
6 | There are some docs/cljs.adoc[extra considerations when using tick from Clojurescript].
7 |
8 | Here is a one-liner to drop into a node repl with tick:
9 |
10 | ---
11 | clj -Sdeps '{:deps {org.clojure/clojurescript {:mvn/version "1.10.764" } tick {:mvn/version "0.4.24-alpha"} }}' -m cljs.main -re node --repl
12 | ---
13 |
14 | == Serialization
15 |
16 | There are many use cases for de/serialization of dates, including simply being able to
17 | copy and paste within the REPL. Tick bundles https://clojars.org/time-literals[time-literals]
18 | Clojure(Script) library, so having require'd tick, in your code or at the repl you can type
19 |
20 | ----
21 | #time/period "P1D"
22 | ----
23 |
24 | which is read as a java.time.Period (or js-joda Period in ClojureScript).
25 |
26 | To avoid tick modifying the printer for java.time objects (if you already employ a custom set of literals for example),
27 | set the following jvm property
28 |
29 | ```
30 | :jvm-opts ["-Dtick.time-literals.printing=false"]
31 | ```
32 |
33 | To read and write edn data containing these literals in Clojure(Script) and for more information generally, see
34 | the https://github.com/henryw374/time-literals[tagged literals Readme]
35 |
36 | include::cljs.adoc[]
--------------------------------------------------------------------------------
/src/tick/calendar.clj:
--------------------------------------------------------------------------------
1 | ;; Copyright © 2016-2018, JUXT LTD.
2 |
3 | (ns tick.calendar
4 | (:require
5 | [tick.core :as t]
6 | [tick.ical :as ical]
7 | [tick.interval :as ival]
8 | [clojure.java.io :as io])
9 | (:import
10 | [java.time DayOfWeek]))
11 |
12 | ;; Now individual calendar sources
13 |
14 | (defn select-by-year
15 | "Given the sequence of holidays (which must be intervals, such as iCalendar VEvent objects), select only those of a given year."
16 | [year holidays]
17 | (let [year (t/year year)]
18 | (filter (fn [hol] (= year (t/year hol))) holidays))
19 | #_(ival/intersection holidays (list (t/year year))))
20 |
21 | ;; TODO: Promote to API
22 | (defn bank-holidays-in-england-and-wales
23 | ([]
24 | (-> "ics/gov.uk/england-and-wales.ics"
25 | io/resource
26 | io/reader
27 | ical/parse-ical
28 | first
29 | ical/events
30 | ))
31 | ([year]
32 | (select-by-year
33 | year
34 | (bank-holidays-in-england-and-wales))))
35 |
36 | ;; TODO: Promote to API
37 | (defn weekend?
38 | "Is the ZonedDateTime during the weekend?"
39 | [dt]
40 | (#{DayOfWeek/SATURDAY DayOfWeek/SUNDAY} (t/day-of-week dt)))
41 |
42 | ;;(t/year (first (bank-holidays-in-england-and-wales)))
43 |
44 | ;;(map ival/as-interval (bank-holidays-in-england-and-wales))
45 |
46 | ;;(ival/as-interval (t/year 2018))
47 |
48 | ;;(count (select-by-year 2018 (bank-holidays-in-england-and-wales)))
49 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | # Clojure CircleCI 2.0 configuration file
2 | #
3 | # Check https://circleci.com/docs/2.0/language-clojure/ for more details
4 | #
5 | version: 2
6 | jobs:
7 | build:
8 | docker:
9 | # specify the version you desire here
10 | - image: circleci/clojure:tools-deps-1.9.0.394-node-browsers
11 |
12 | # Specify service dependencies here if necessary
13 | # CircleCI maintains a library of pre-built images
14 | # documented at https://circleci.com/docs/2.0/circleci-images/
15 | # - image: circleci/postgres:9.4
16 |
17 | working_directory: ~/repo
18 |
19 | environment:
20 | # Customize the JVM maximum heap limit
21 | JVM_OPTS: -Xmx3200m
22 |
23 | steps:
24 | - checkout
25 |
26 | # Download and cache dependencies
27 | - restore_cache:
28 | keys:
29 | - v1-dependencies-{{ checksum "deps.edn" }}
30 | # fallback to using the latest cache if no exact match is found
31 | - v1-dependencies-
32 |
33 | # need a few bits for cljs test - there must be a better way of including?
34 | - run: sudo npm install karma-cli -g
35 | - run: npm install karma --save-dev
36 | - run: npm install karma-chrome-launcher
37 | - run: npm install karma-cljs-test
38 |
39 | - run: sudo apt-get install -y make
40 | - run: make test
41 |
42 | - save_cache:
43 | paths:
44 | - ~/.m2
45 | key: v1-dependencies-{{ checksum "deps.edn" }}
46 |
--------------------------------------------------------------------------------
/test/tick/ical_test.clj:
--------------------------------------------------------------------------------
1 | ;; Copyright © 2016-2017, JUXT LTD.
2 |
3 | (ns tick.ical-test
4 | (:require
5 | [tick.ical :as ical]
6 | [clojure.spec.alpha :as s]
7 | [clojure.java.io :as io]
8 | [tick.core :as t]
9 | [clojure.test :refer :all]))
10 |
11 | (deftest parse-dtstart
12 | (let [{:keys [name params value]} (ical/line->contentline "DTSTART;TZID=US-EAST:20180116T140000")]
13 | (is (= "DTSTART" name))
14 | (is (= "US-EAST" (get params "TZID")))
15 | (is (= "20180116T140000" value))))
16 |
17 | (deftest parse-unicode
18 | (let [{:keys [name params value]} (ical/line->contentline "SUMMARY;LANGUAGE=en-us:United Kingdom: St Patrick�s Day (substitute day) (Regional)")]
19 | (is (= "SUMMARY" name))
20 | (is (= "en-us" (get params "LANGUAGE")))
21 | (is (= "United Kingdom: St Patrick�s Day (substitute day) (Regional)" value))))
22 |
23 | (deftest stress-test
24 | (is (= 532
25 | (count
26 | (for [line (map first (partition 10 (ical/unfolding-line-seq (io/reader (io/resource "gb.ics")))))]
27 | (ical/line->contentline line))))))
28 |
29 | (deftest ^:tick.test/slow parse-icalendar-test
30 | (let [result (ical/parse-ical (io/reader (io/resource "gb.ics")))]
31 | (is (= 231 (-> result first :subobjects count)))))
32 |
33 |
34 | #_(for [obj (:subobjects (first (ical/parse-icalendar (io/reader (io/resource "ics/gov.uk/england-and-wales.ics")))))]
35 | [(:object obj)
36 | (:value (first (ical/property obj :summary)))
37 | (:value (first (ical/property obj :dtstart)))
38 | ;; obj
39 | ;; (:value (first (ical/property obj :summary)))
40 | ]
41 | )
42 |
--------------------------------------------------------------------------------
/dev/resources/clock-34354.svg:
--------------------------------------------------------------------------------
1 |
2 |
16 |
--------------------------------------------------------------------------------
/docs/cookbook/misc.adoc:
--------------------------------------------------------------------------------
1 | == Miscellaneous
2 |
3 | [.lead]
4 | These examples don't have a home yet.
5 |
6 | ====
7 | Check if an expiration has passed:
8 | [source.code,clojure]
9 | ----
10 | (let [expiry (t/instant "2018-01-01T00:00")]
11 | (t/> (t/now)
12 | expiry))
13 | ----
14 | ====
15 |
16 | ====
17 | Return a sequence of dates between two given dates a with
18 | a specified jump between each.
19 |
20 | For instance, to get a sequence of the first day of each month in a given year:
21 |
22 | [source.code,clojure]
23 | ----
24 | (def intvl (t/bounds (t/year)))
25 | (t/range (t/beginning intvl)
26 | (t/end intvl)
27 | (t/new-period 1 :months))
28 | ----
29 | ====
30 |
31 |
32 | ====
33 | Get the time difference between two instances:
34 | [source.code,clojure]
35 | ----
36 | (t/between (t/now) (t/epoch))
37 | ----
38 | ====
39 |
40 |
41 | ====
42 | Not sure on input format? `parse` will do the work for you.
43 |
44 | [source.code,clojure]
45 | ----
46 | (t/parse "2 pm")
47 | ----
48 | [source.code,clojure]
49 | ----
50 | (t/parse "14")
51 | ----
52 | [source.code,clojure]
53 | ----
54 | (t/parse "14:00")
55 | ----
56 | [source.code,clojure]
57 | ----
58 | (t/parse "2018-01-01")
59 | ----
60 | [source.code,clojure]
61 | ----
62 | (t/parse "2018-01-01T00:00")
63 | ----
64 | [source.code,clojure]
65 | ----
66 | (t/parse "2018-01-01T00:00:00")
67 | ----
68 | [source.code,clojure]
69 | ----
70 | (t/parse "2018-01-01T00:00:00+01:00")
71 | ----
72 | [source.code,clojure]
73 | ----
74 | (t/parse "2018-01-01T00:00:00+01:00[Europe/London]")
75 | ----
76 | [source.code,clojure]
77 | ----
78 | (t/parse "2019")
79 | ----
80 | [source.code,clojure]
81 | ----
82 | (t/parse "2000-01")
83 | ----
84 | ====
85 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # Authoritative build rules for tick.
2 |
3 | # If you don't have GNU Make on your system, use this file as a
4 | # cribsheet for how to build various aspects of tick.
5 |
6 | STYLESDIR = ../asciidoctor-stylesheet-factory/stylesheets
7 | STYLESHEET = juxt.css
8 |
9 | .PHONY: watch default deploy test dev-docs-cljs
10 |
11 | default: docs/index.html
12 |
13 | # Build the docs
14 | docs/index.html: docs/*.adoc docs/docinfo*.html ${STYLESDIR}/${STYLESHEET}
15 | asciidoctor -d book \
16 | -a "webfonts!" \
17 | -a stylesdir=../${STYLESDIR} \
18 | -a stylesheet=${STYLESHEET} \
19 | docs/index.adoc
20 |
21 | test-clj:
22 | clojure -Atest -e deprecated
23 | test-chrome:
24 | rm -rf cljs-test-runner-out && mkdir -p cljs-test-runner-out/gen && clojure -Sverbose -Atest-chrome
25 | test-node:
26 | rm -rf cljs-test-runner-out && mkdir -p cljs-test-runner-out/gen && clojure -Sverbose -Atest-node
27 | test:
28 | make test-clj && make test-chrome && make test-node
29 |
30 | # For developing the cljs used by the documentation, add --repl and change docs.cljs.edn optimizations to :none to develop interactively
31 | dev-docs-cljs:
32 | clojure -Adocs-index
33 |
34 | pom:
35 | rm pom.xml; clojure -Spom; echo "Now use git diff to add back in the non-generated bits of pom"
36 | deploy:
37 | mvn deploy
38 | nrepl:
39 | clj -Adev:dev-nrepl -m nrepl.cmdline --middleware "[cider.piggieback/wrap-cljs-repl]" --port 5610
40 |
41 | # hooray for stackoverflow
42 | .PHONY: list
43 | list:
44 | @$(MAKE) -pRrq -f $(lastword $(MAKEFILE_LIST)) : 2>/dev/null | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' | sort | egrep -v -e '^[^[:alnum:]]' -e '^$@$$' | xargs
45 |
--------------------------------------------------------------------------------
/docs/cookbook/intervals.adoc:
--------------------------------------------------------------------------------
1 | == Intervals
2 |
3 | An interval in time is a duration that has a specified beginning and end.
4 |
5 | === Create an interval
6 |
7 | There are multiple ways an interval can be created in tick:
8 |
9 | ====
10 | Specify the beginning and the end:
11 |
12 | [source.code,clojure]
13 | ----
14 | (t/new-interval (t/date-time "2000-01-01T00:00")
15 | (t/date-time "2001-01-01T00:00"))
16 | ----
17 |
18 | [source.code,clojure]
19 | ----
20 | {:tick/beginning (t/date-time "2000-01-01T00:00")
21 | :tick/end (t/date-time "2001-01-01T00:00")}
22 | ----
23 |
24 | [source.code,clojure]
25 | ----
26 | (t/bounds (t/year 2000))
27 | ----
28 |
29 | All of the above result in the same interval:
30 |
31 | [source.code,clojure]
32 | ----
33 | (= (t/new-interval (t/date-time "2000-01-01T00:00")
34 | (t/date-time "2001-01-01T00:00"))
35 | (t/bounds (t/year 2000))
36 | {:tick/beginning (t/date-time "2000-01-01T00:00")
37 | :tick/end (t/date-time "2001-01-01T00:00")})
38 | ----
39 | ====
40 |
41 | === Interval Manipulation:
42 |
43 | The duration of an interval can be modified using `extend`.
44 | ====
45 | Extend an instant to a interval
46 | [source.code,clojure]
47 | ----
48 | (t/extend (t/instant "2000-01-01T00:00")
49 | (t/new-period 3 :weeks))
50 | ----
51 |
52 | Extend an interval:
53 | [source.code,clojure]
54 | ----
55 | (t/extend (t/bounds (t/year 2000)) (t/new-period 1 :years))
56 | ----
57 |
58 | Shorten an interval:
59 | [source.code,clojure]
60 | ----
61 | (t/extend (t/bounds (t/year 2000)) (t/new-period -1 :months))
62 | ----
63 | ====
64 |
65 | The beginning of an interval can be modified whilst preserving the duration.
66 |
67 | ====
68 | Shift the interval back in time:
69 | [source.code,clojure]
70 | ----
71 | (t/<< (t/bounds (t/year 2000)) (t/new-period 6 :months))
72 | ----
73 |
74 | Or forward in time:
75 | [source.code,clojure]
76 | ----
77 | (t/>> (t/bounds (t/today)) (t/new-duration 1 :half-days))
78 | ----
79 | ====
80 |
--------------------------------------------------------------------------------
/src/tick/deprecated/timeline.clj:
--------------------------------------------------------------------------------
1 | ;; Copyright © 2016-2017, JUXT LTD.
2 |
3 | (ns tick.deprecated.timeline
4 | (:require
5 | [clojure.spec.alpha :as s])
6 | (:import
7 | [java.time ZonedDateTime]
8 | [java.time.temporal Temporal TemporalAmount]))
9 |
10 | (defn periodic-seq
11 | "Given a start time, create a timeline with times at constant intervals of period length"
12 | ([^Temporal start ^TemporalAmount period]
13 | (iterate #(.addTo period %) start)))
14 |
15 | (s/def :tick/date #(instance? ZonedDateTime %))
16 |
17 | (defn timeline-xf
18 | "A transducer that transforms a sequence of :tick/date into a
19 | timeline of Clojure maps. Each map contains :tick/date."
20 | [rf]
21 | (fn
22 | ([] (rf))
23 | ([result] (rf result))
24 | ([result input]
25 | (rf result {:tick/date input}))))
26 |
27 | (s/def :tick/seq integer?) ; TODO: postive too
28 |
29 | (defn sequencer
30 | "A transducer that adds a :tick/seq to a timeline."
31 | ([] (sequencer 0))
32 | ([start]
33 | (fn [rf]
34 | (let [counter (volatile! (dec start))]
35 | (fn
36 | ([] (rf))
37 | ([result] (rf result))
38 | ([result input]
39 | (rf result (assoc input :tick/seq (vswap! counter inc)))))))))
40 |
41 | (defn interleave-timelines
42 | "Interleave a collection of timelines producing a single timeline ordered by time.
43 | See http://blog.malcolmsparks.com/?p=42 for further discussion."
44 | [& timelines]
45 | (let [begin (new Object)
46 | end (new Object)]
47 | (letfn [(next-item [[_ timelines]]
48 | (if (nil? timelines)
49 | [end nil]
50 | (let [[[yield & p] & q]
51 | (sort-by (comp :tick/date first) compare timelines)]
52 | [yield (if p (cons p q) q)])))]
53 | (->> timelines
54 | (vector begin)
55 | (iterate next-item)
56 | (drop 1)
57 | (map first)
58 | (take-while (partial not= end))))))
59 |
60 | (defn timeline
61 | "Create a timeline from a sequence of dates (java.time.ZonedDateTime)"
62 | ([coll]
63 | (timeline nil coll))
64 | ([xf coll]
65 | (sequence (cond-> timeline-xf xf (comp xf)) coll)))
66 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 | tick
5 | tick
6 | 0.4.26-alpha
7 | tick
8 | A Clojure(Script) library for dealing with time. Intended as a replacement for clj-time
9 | https://github.com/juxt/tick
10 |
11 | https://github.com/juxt/tick
12 |
13 |
14 |
15 | org.clojure
16 | clojure
17 | 1.9.0
18 |
19 |
20 | cljc.java-time
21 | cljc.java-time
22 | 0.1.9
23 |
24 |
25 | cljsjs
26 | js-joda-timezone
27 | 2.2.0-0
28 |
29 |
30 | cljsjs
31 | js-joda-locale-en-us
32 | 3.1.1-1
33 |
34 |
35 | time-literals
36 | time-literals
37 | 0.1.3
38 |
39 |
40 | cljs.java-time
41 | cljs.java-time
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | src
50 |
51 |
52 | resources
53 |
54 |
55 |
56 |
57 |
58 |
59 | clojars
60 | Clojars repository
61 | https://clojars.org/repo
62 |
63 |
64 |
65 |
66 | clojars
67 | https://repo.clojars.org/
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/test/tick/deprecated/timeline_test.clj:
--------------------------------------------------------------------------------
1 | ;; Copyright © 2016, JUXT LTD.
2 |
3 | (ns tick.deprecated.timeline-test
4 | (:require
5 | [clojure.test :refer :all]
6 | [tick.deprecated.timeline :refer [interleave-timelines periodic-seq timeline sequencer]]
7 | [tick.deprecated.clock :refer [just-now]]
8 | [tick.deprecated.cal :as cal]
9 | [tick.core :refer [new-duration]])
10 | (:import
11 | [java.time Clock ZoneId Instant Duration DayOfWeek Month ZonedDateTime LocalDate LocalDateTime]
12 | [java.time.temporal ChronoField ChronoUnit]))
13 |
14 | (def LONDON (ZoneId/of "Europe/London"))
15 |
16 | (def T0 (-> "2012-12-04T05:21:00" LocalDateTime/parse (.atZone LONDON)))
17 | (def T1 (.plusSeconds T0 10))
18 |
19 | (deftest ^:deprecated periodic-seq-test
20 | (let [sq (periodic-seq T0 (new-duration 1 :minutes))]
21 | (testing "sq starts with start time"
22 | (is (= T0 (first sq))))
23 | (testing "sq moves forward by 10 minutes"
24 | (is (= (-> "2012-12-04T05:31:00" LocalDateTime/parse (.atZone LONDON))
25 | (first (drop 10 sq)))))))
26 |
27 | (defn acceptable-hours [zdt]
28 | (let [h (.getHour zdt)]
29 | (<= 7 h 21)))
30 |
31 | (deftest ^:deprecated composition-test
32 | (testing "Filter by acceptable hours"
33 | (is (= 62 (count
34 | (->> (periodic-seq T0 (new-duration 1 :hours))
35 | (take 100)
36 | (filter acceptable-hours)))))))
37 |
38 | (deftest ^:deprecated interleave-timelines-test
39 | (let [merged
40 | (interleave-timelines
41 | (take 10 (timeline (periodic-seq (.plus (just-now) (new-duration 10 :seconds)) (new-duration 1 :minutes))))
42 | (take 10 (timeline (periodic-seq (just-now) (new-duration 1 :minutes)))))]
43 | (is (distinct? merged))
44 | (is (= 20 (count merged)))))
45 |
46 | (deftest ^:deprecated sequencer-test
47 | (is (= 0 (:tick/seq (first (sequence (sequencer)
48 | (interleave-timelines
49 | (timeline (map cal/easter-monday (iterate inc 2012)))
50 | (timeline (map cal/good-friday (iterate inc 2012)))))))))
51 | (is (= 10 (:tick/seq (first (sequence (sequencer 10)
52 | (interleave-timelines
53 | (timeline (map cal/easter-monday (iterate inc 2012)))
54 | (timeline (map cal/good-friday (iterate inc 2012))))))))))
55 |
--------------------------------------------------------------------------------
/docs/durations.adoc:
--------------------------------------------------------------------------------
1 | = Durations & periods
2 |
3 | A *Duration* instance stores time as an amount of *seconds*, for example 5.999999999 seconds.
4 |
5 | A *Period* instance stores amounts of *years*, *months* and *days*, for example -1 years, 20 months and 100 days
6 |
7 | The javadocs refer to these entities as *time-based* and *date-based*, respectively.
8 |
9 | The reason for having both representations is that the Period units are variable length (leap years, DST etc) but the time-based ones are not.
10 |
11 | So for example a Duration of 48 hours will not the same span as a Period of 2 days in all contexts.
12 |
13 | Note that https://www.threeten.org/threeten-extra/[threeten-extra] has an additional PeriodDuration entity
14 |
15 | == Construction
16 |
17 | [%header,cols="l,a,l"]
18 | |===
19 | |Code|Description|Return type
20 | |(t/new-duration 1 :seconds)|Duration of a second|java.time.Duration
21 | |(t/new-duration 100 :days)|Duration of 100 days|java.time.Duration
22 | |(t/new-period 100 :days)|Period of 100 days|java.time.Period
23 | |(t/new-period 2 :months)|Period of 2 months|java.time.Period
24 | |===
25 |
26 | === Days, Months, Years…
27 |
28 | Instances of other `java.time` types are readily constructed with _tick_.
29 |
30 | [%header,cols="l,a,l"]
31 | |===
32 | |Example|Description|Return type
33 | |(day "mon")|Monday|java.time.DayOfWeek
34 | |(month "August")|August|java.time.Month
35 | |(month 12)|December|java.time.Month
36 | |(year-month "2012-12")|December 2012|java.time.YearMonth
37 | |(year 1999)|The year 1999|java.time.Year
38 | |===
39 |
40 | == Derivation
41 |
42 | * Add durations to durations
43 |
44 | == Comparison
45 |
46 | NOTE: TBD
47 |
48 | == Misc
49 |
50 | NOTE: TODO Don't forget you can create zone-offsets from durations!
51 |
52 | ====
53 | NOTE: TODO Don't forget you can create instants from durations - this is often needed when you get Unix times (e.g. JWT OAuth2 tokens)
54 |
55 | The problem with numeric times is that there are cases where the units
56 | are in seconds and cases where milliseconds are used. If _tick_ were
57 | to convert numbers to times, it would be a source of confusion and
58 | bugs if the units were not clear. For this reason, you cannot convert
59 | numbers to times. However, you can first create the duration from the
60 | number, specifying the units explicitly, and then convert the duration
61 | to an `instant` (or `inst`).
62 |
63 | [source,clojure]
64 | ----
65 | (instant (new-duration 1531467976048 :millis))
66 | ----
67 |
68 | [source,clojure]
69 | ----
70 | (inst (new-duration 1531468976 :seconds))
71 | ----
72 | ====
73 |
--------------------------------------------------------------------------------
/docs/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
37 |
--------------------------------------------------------------------------------
/docs/logo-normal.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
37 |
--------------------------------------------------------------------------------
/dev/src/tick/viz.clj:
--------------------------------------------------------------------------------
1 | (ns tick.viz
2 | (:require
3 | [clojure.xml :refer [parse]]
4 | [clojure.java.io :as io]
5 | [tick.alpha.api :as t]
6 | [clojure.data.xml :as x]
7 | )
8 | (:import
9 | [org.apache.batik.swing JSVGCanvas]
10 | [javax.xml.parsers DocumentBuilder DocumentBuilderFactory]
11 | ))
12 |
13 | (x/alias-uri :svg "http://www.w3.org/2000/svg")
14 |
15 | (def canvas (JSVGCanvas.))
16 |
17 | (defn show-canvas []
18 | (let [frame (javax.swing.JFrame.)]
19 | (.add (.getContentPane frame) canvas)
20 | (.setSize frame 800 200)
21 | (.setDefaultCloseOperation frame javax.swing.JFrame/HIDE_ON_CLOSE)
22 | (.setVisible frame true)))
23 |
24 | (defn until-seconds [a b]
25 | (.until a b (t/units :seconds)))
26 |
27 | (defn label [obj label]
28 | (with-meta obj {:label label}))
29 |
30 | (defn view [& ival-sets]
31 | (let [tmpfile (java.io.File/createTempFile "tick" ".svg" (io/file (System/getProperty "java.io.tmpdir")))
32 | _ (assert (every? t/ordered-disjoint-intervals? ival-sets))
33 | left-edge (apply t/min (map first (apply concat ival-sets)))
34 | right-edge (apply t/max (map second (apply concat ival-sets)))
35 | width (until-seconds left-edge right-edge)
36 | scale (float (/ 800 width))
37 | bar-height 30
38 | bar-margin 40]
39 |
40 | (with-open [f (java.io.PrintWriter. (io/writer tmpfile))]
41 | (x/emit
42 | {:tag ::svg/svg
43 | :attrs {:viewBox "0 0 800 800"}
44 | :content
45 | (map-indexed
46 | (fn [y ivals]
47 | (for [[x1 x2 :as ival] ivals]
48 | {:tag ::svg/g
49 | :content
50 | (let [x (* scale (until-seconds left-edge x1))
51 | y (+ bar-margin (* y (+ bar-margin bar-height)))
52 | width (* scale (- (until-seconds left-edge x2)
53 | (until-seconds left-edge x1)))]
54 | [{:tag ::svg/rect
55 | :attrs {:x x
56 | :y y
57 | :width width
58 | :height bar-height
59 | :fill "#aaf"
60 | :stroke "#88f"
61 | :stroke-width "1px"}}
62 |
63 | (when-some [text (-> ival meta :label)]
64 | {:tag ::svg/text
65 | :attrs {:x (+ x (/ width 2))
66 | :y (- y (/ bar-height 8))
67 | :text-anchor "middle"
68 | :font-size "130%"}
69 |
70 | :content (str text)})])}))
71 |
72 | ival-sets)}
73 | f))
74 |
75 | (.loadSVGDocument canvas (str (.toURL tmpfile)))))
76 |
--------------------------------------------------------------------------------
/docs/cookbook/arithmetick.adoc:
--------------------------------------------------------------------------------
1 | == Arithmetic
2 |
3 | The tick library lends itself to doing additions, subtractions
4 | and divisions of time chunks and durations. Below are some
5 | examples of how time can be treated as a quantity which can be operated
6 | on.
7 |
8 | === Simple maths
9 |
10 | Operating on an instant it will return another instant in time.
11 |
12 | ====
13 | Addition:
14 | [source.code,clojure]
15 | ----
16 | (t/+ (t/now)
17 | (t/new-duration 15 :minutes))
18 | ----
19 | Subtraction:
20 | [source.code,clojure]
21 | ----
22 | (t/- (t/now)
23 | (t/new-duration 10 :days))
24 | ----
25 | ====
26 |
27 | An interval has a beginning and an end, operating on it
28 | will return a modified interval.
29 |
30 | ====
31 | Addition:
32 | [source.code,clojure]
33 | ----
34 | (t/extend {:tick/beginning (t/instant "2018-01-01T00:00")
35 | :tick/end (t/instant "2018-01-10T00:00")}
36 | (t/new-period 10 :weeks))
37 | ----
38 | Subtraction:
39 | [source.code,clojure]
40 | ----
41 | (t/extend {:tick/beginning (t/instant "2018-01-01T00:00")
42 | :tick/end (t/instant "2018-01-10T00:00")}
43 | (t/new-duration -1 :days))
44 | ----
45 | This can be done with `scale` too:
46 |
47 | [source.code,clojure]
48 | ----
49 | (= (t/extend (t/today)
50 | (t/new-period 10 :weeks))
51 | (t/scale (t/today)
52 | (t/new-period 10 :weeks)))
53 | ----
54 |
55 | ====
56 |
57 | An interval can be divided into smaller intervals:
58 |
59 | ====
60 | Divide the day by 24, to get hour long intervals:
61 |
62 | ----
63 | (map #(apply t/new-interval %)
64 | (t/divide-by 24 {:tick/beginning (t/instant "2000-01-01T00:00")
65 | :tick/end (t/instant "2000-01-02T00:00")}))
66 | ----
67 |
68 | Or just divide the day by a duration of 1 hour to get the same result:
69 | [source.code,clojure]
70 | ----
71 | (= (t/divide-by (t/new-duration 1 :hours)
72 | {:tick/beginning (t/instant "2000-01-01T00:00")
73 | :tick/end (t/instant "2000-01-02T00:00")})
74 | (t/divide-by 24
75 | {:tick/beginning (t/instant "2000-01-01T00:00")
76 | :tick/end (t/instant "2000-01-02T00:00")}))
77 | ----
78 | ====
79 |
80 | Durations can be treated like independent chunks of time.
81 | They can be extended, shrunk and divided.
82 |
83 | ====
84 | Addition:
85 | [source.code,clojure]
86 | ----
87 | (t/+ (t/new-duration 1 :hours)
88 | (t/new-duration 10 :minutes))
89 | ----
90 | Subtraction:
91 | [source.code,clojure]
92 | ----
93 | (t/- (t/new-duration 1 :hours)
94 | (t/new-duration 10 :minutes))
95 | ----
96 | Division:
97 | [source.code,clojure]
98 | ----
99 | (t/divide (t/new-duration 1 :hours)
100 | (t/new-duration 1 :minutes))
101 | ----
102 | ====
103 |
--------------------------------------------------------------------------------
/src/tick/format.cljc:
--------------------------------------------------------------------------------
1 | (ns tick.format
2 | "originally copied from https://github.com/dm3/clojure.java-time"
3 | (:refer-clojure :exclude (format))
4 | #?(:cljs (:require [java.time.format :refer [DateTimeFormatter]]))
5 | #?(:clj
6 | (:import [java.time.format DateTimeFormatter]
7 | [java.util Locale])))
8 |
9 | (def predefined-formatters
10 | {:iso-zoned-date-time (. DateTimeFormatter -ISO_ZONED_DATE_TIME)
11 | :iso-offset-date-time (. DateTimeFormatter -ISO_OFFSET_DATE_TIME)
12 | :iso-local-time (. DateTimeFormatter -ISO_LOCAL_TIME)
13 | :iso-local-date-time (. DateTimeFormatter -ISO_LOCAL_DATE_TIME)
14 | :iso-local-date (. DateTimeFormatter -ISO_LOCAL_DATE)
15 | :iso-instant (. DateTimeFormatter -ISO_INSTANT)
16 |
17 | ; these exist in java but not in js-joda
18 | ;:iso-offset-date (. DateTimeFormatter -ISO_OFFSET_DATE)
19 | ;:rfc-1123-date-time (. DateTimeFormatter -RFC_1123_DATE_TIME)
20 | ;:iso-week-date (. DateTimeFormatter -ISO_WEEK_DATE)
21 | ;:iso-ordinal-date (. DateTimeFormatter -ISO_ORDINAL_DATE)
22 | ;:iso-time (. DateTimeFormatter -ISO_TIME)
23 | ;:iso-date (. DateTimeFormatter -ISO_DATE)
24 | ;:basic-iso-date (. DateTimeFormatter -BASIC_ISO_DATE)
25 | ;:iso-date-time (. DateTimeFormatter -ISO_DATE_TIME)
26 | ;:iso-offset-time (. DateTimeFormatter -ISO_OFFSET_TIME)
27 | })
28 |
29 | (defn ^DateTimeFormatter formatter
30 | "Constructs a DateTimeFormatter out of either a
31 |
32 | * format string - \"YYYY/mm/DD\" \"YYY HH:MM\" etc.
33 | or
34 | * formatter name - :iso-instant :iso-local-date etc
35 |
36 | and a Locale, which is optional."
37 | ([fmt]
38 | (formatter
39 | fmt
40 | #?(:clj (Locale/getDefault)
41 | :cljs (try
42 | (some->
43 | (goog.object/get js/JSJodaLocale "Locale")
44 | (goog.object/get "US"))
45 | (catch js/Error e)))))
46 | ([fmt locale]
47 | (let [^DateTimeFormatter fmt
48 | (cond (instance? DateTimeFormatter fmt) fmt
49 | (string? fmt) (if (nil? locale)
50 | (throw
51 | #?(:clj (Exception. "Locale is nil")
52 | :cljs (js/Error. (str "Locale is nil, try adding a require '[tick.locale-en-us]"))))
53 | (.. DateTimeFormatter
54 | (ofPattern fmt)
55 | (withLocale locale)))
56 | :else (get predefined-formatters fmt))]
57 | fmt)))
58 |
59 | (defn format
60 | "Formats the given time entity as a string.
61 | Accepts something that can be converted to a `DateTimeFormatter` as a first
62 | argument. Given one argument uses the default format."
63 | ([o] (str o))
64 | ([fmt o]
65 | (.format (formatter fmt) o)))
--------------------------------------------------------------------------------
/docs/clocks.adoc:
--------------------------------------------------------------------------------
1 | = Clocks
2 |
3 | In tick, clocks are used for getting the current time, in a given
4 | time-zone. You should prefer using clocks to making direct calls to
5 | `(System/currentTimeMillis)`, because this then allows you and others
6 | to plugin alternative clocks, perhaps for testing purposes.
7 |
8 | You create a clock that tracks the current time.
9 |
10 | ----
11 | (clock)
12 | ----
13 |
14 | With an argument, you can fix a clock to always report a fixed time.
15 |
16 | ----
17 | (clock "1999-12-31T23:59:59")
18 | ----
19 |
20 | == Construction
21 |
22 | [%header,cols="l,a,l"]
23 | |===
24 | |Code|Description|Return type
25 | |(clock)|Return a clock that will always return the current time|java.time.Clock
26 | |===
27 |
28 | == Derivation
29 |
30 | Just like times and dates, you can time-shift clocks forward and
31 | backward using the `>>` and `<<` functions respectively.
32 |
33 | ----
34 | (<< (clock) (hours 2))
35 | ----
36 |
37 | You can also create a clock from a base clock which reports time with granualarity given by a duration.
38 |
39 | ----
40 | (def minute-clk (tick (clock) (minutes 1)))
41 | ----
42 |
43 | [%header,cols="l,a,l"]
44 | |===
45 | |Code|Description|Return type
46 | |(<< (clock) (minutes 2))|Return a clock running 2 minutes slow|java.time.Clock
47 | |(>> (clock) (minutes 2))|Return a clock running 2 minutes fast|java.time.Clock
48 | |===
49 |
50 | == Comparison
51 |
52 | NOTE: TBD
53 |
54 | == Atomic clocks?
55 |
56 | In Clojure, an atom is a holder of a value at a particular time. Similarly, a _tick_ atom is a clock holding the clock's time, which is constantly changing.
57 |
58 | You create this atom with `(atom)`. Naturally, you can get the instant of the atom's clock by dereferencing, e.g. `@(atom)`
59 |
60 | ----
61 | user> (def clk (atom))
62 | user> (println @clk)
63 | #object[java.time.Instant 0x2e014670 2018-02-28T07:52:52.302Z]
64 | (some time later)
65 | user> (println @clk)
66 | #object[java.time.Instant 0x6e5b1dca 2018-02-28T08:01:50.622Z]
67 | ----
68 |
69 | You can also create an atom with a clock.
70 |
71 | ----
72 | (atom (clock))
73 | ----
74 |
75 | [%header,cols="l,a,l"]
76 | |===
77 | |Code|Description|Return type
78 | |(atom)|Return a clock that tracks the current time|java.time.Clock
79 | |===
80 |
81 |
82 | == Substitution
83 |
84 | A clock can be used to callibrate tick to a particular time and time-zone, if system defaults are not desired.
85 |
86 | As I'm currently writing this in London, on my system I get the following when I use '(zone)'.
87 |
88 | ----
89 | (zone)
90 |
91 | => #object[java.time.ZoneRegion 0x744a6545 "Europe/London"]
92 | ----
93 |
94 | However, if we wanted to test in New York, we can set the clock to exist in that time-zone:
95 |
96 | ----
97 | (t/with-clock (-> (t/clock) (t/in "America/New_York"))
98 | (t/zone))
99 |
100 | => #object[java.time.ZoneRegion 0x5a9d412 "America/New_York"]
101 | ----
102 |
--------------------------------------------------------------------------------
/test/tick/alpha/api/dates_test.cljc:
--------------------------------------------------------------------------------
1 | ;; Copyright © 2016-2018, JUXT LTD.
2 |
3 | (ns tick.alpha.api.dates-test
4 | (:refer-clojure :exclude [dec < range <= min long int > extend - time / >= inc + max complement atom swap-vals! reset-vals! compare-and-set! reset! swap! second group-by conj])
5 | (:require
6 | [clojure.spec.alpha :as s]
7 | #?(:clj [clojure.test :refer :all]
8 | :cljs [cljs.test :refer-macros [deftest is testing run-tests]])
9 | #?@(:cljs
10 | [[java.time :refer [Clock LocalTime LocalDateTime ]]
11 | [tick.timezone]])
12 | [tick.alpha.api :as t :refer [with-clock] :refer-macros [with-clock]])
13 | #?(:clj (:import [java.time Clock LocalTime LocalDateTime])))
14 |
15 | ;; See doc/dates.adoc
16 |
17 | (deftest time-construction-test
18 | (testing "(time)"
19 | (is (instance? LocalTime (t/time))))
20 | (testing "(time \"4pm\")"
21 | (is (instance? LocalTime (t/time "4pm")))
22 | (is (= "16:00" (str (t/time "4pm")))))
23 | (testing "(midnight)"
24 | (is (instance? LocalTime (t/midnight)))
25 | (is (= "00:00" (str (t/midnight)))))
26 | (testing "(noon)"
27 | (is (instance? LocalTime (t/noon)))
28 | (is (= "12:00" (str (t/noon))))))
29 |
30 | (deftest date-construction-test
31 | (is (instance? LocalDateTime (t/noon (t/today))))
32 | (with-clock (-> (t/date "2018-02-14") (t/at "10:00"))
33 | (testing "(noon (today))"
34 | (is (= "2018-02-14T12:00" (str (t/noon (t/today))))))
35 | (testing "(noon (date))"
36 | (is (= "2018-02-14T12:00" (str (t/noon (t/date))))))))
37 |
38 | ;; TODO: Clock tests
39 | ;; Create with a value for a fixed clock. Value can be a time or a zone
40 |
41 | (deftest clock-test
42 | (testing "clock"
43 | (with-clock (-> (t/date "2018-02-14") (t/at "10:00") (t/in "America/New_York"))
44 | (testing "(clock) return type"
45 | (is (instance? Clock (t/clock))))
46 | (testing "Time shifting the clock back by 2 hours"
47 | (is (= "2018-02-14T13:00:00Z" (str (t/instant (t/<< (t/clock) (t/new-duration 2 :hours)))))))
48 | (testing "with instant"
49 | (is (= (t/zone (t/clock (t/instant)))
50 | (t/zone "America/New_York"))))))
51 |
52 | (testing "Converting using with-clock"
53 | (t/with-clock (t/clock (t/zone "America/New_York"))
54 | (testing "inst to zoned-date-time"
55 | (is (= (t/zoned-date-time #inst"2019-08-07T16:00")
56 | (t/zoned-date-time "2019-08-07T12:00-04:00[America/New_York]"))))
57 | (testing "date-time to zoned-date-time"
58 | (is (= (t/zoned-date-time (t/date-time "2019-08-07T12:00"))
59 | (t/zoned-date-time "2019-08-07T12:00-04:00[America/New_York]"))))
60 | (testing "date-time to offset-date-time"
61 | (is (= (t/offset-date-time (t/date-time "2019-08-07T12:00"))
62 | #?(:clj (t/offset-date-time "2019-08-07T12:00-04:00")
63 | :cljs (t/zoned-date-time "2019-08-07T12:00-04:00[America/New_York]")))))))
64 |
65 | (testing "Creating a clock with a zone, and returning that zone"
66 | (is (= "America/New_York" (str (t/zone (t/clock (t/zone "America/New_York")))))))
67 |
68 | (testing "Creation of clock with fixed instant"
69 | (is (= "2017-10-31T16:00:00Z" (str (t/instant (t/clock "2017-10-31T16:00:00Z")))))))
70 |
71 |
72 | ;; TODO: tick function
73 |
74 | ;; TODO: Atomic clocks
75 |
--------------------------------------------------------------------------------
/docs/intervals.adoc:
--------------------------------------------------------------------------------
1 | = Intervals
2 |
3 | In _tick_, an interval is a span of time defined by two points in time, the first being before the second.
4 |
5 | Intervals are maps containing both a `tick/beginning` and a `tick/end` entry. This flexible design allows any Clojure map to be treated as an interval.
6 |
7 | Intervals can be represented two local times as well as instants.
8 |
9 | == Construction
10 |
11 | Obviously, the Clojure's literal syntax for maps can be used to create intervals.
12 |
13 | ====
14 | Here we use a literal map syntax to construct an interval representing the last 5 minutes of 2018 (in UTC).
15 |
16 | [source,clojure]
17 | ----
18 | {:tick/beginning "2018-12-31T23:55:00Z"
19 | :tick/end "2019-01-01T00:00:00Z"}
20 | ----
21 |
22 | ====
23 |
24 | Alternatively, we can use the `t/new-interval` function which takes the two boundaries of the interval as its arguments.
25 |
26 | ====
27 | [source,clojure]
28 | ----
29 | (t/new-interval
30 | (t/instant "2018-12-31T23:55:00Z")
31 | (t/instant "2019-01-01T00:00:00Z"))
32 | ----
33 | ====
34 |
35 | == Derivation
36 |
37 | Dates, months and years can also be considered to be themselves ranges, and can be converted to intervals with the `t/bounds` function.
38 |
39 | ====
40 | To return today as an interval:
41 |
42 | [source.code,clojure]
43 | ----
44 | (t/bounds (t/today))
45 | ----
46 | ====
47 |
48 | The arguments to `t/new-interval` do not have to be instants, they can be any time supported by _tick_.
49 |
50 | ====
51 | To return a 2-day interval spanning midnight this morning to midnight [#eval-two-days-from-today]#two days from today#:
52 | // Calculate the day today plus 2 days
53 |
54 | [source.code,clojure]
55 | ----
56 | (t/new-interval (t/today) (t/tomorrow))
57 | ----
58 | ====
59 |
60 | == Comparison
61 |
62 | Two intervals can be compared against each other with the `t/relation` function. link:https://en.wikipedia.org/wiki/Allen%27s_interval_algebra[Allen's interval algebra] tells us there are 13 possible relations between two intervals.
63 |
64 | .Interval relations
65 | ====
66 | Consider the time-span represented by the word 'yesterday' and compare it to the time-span represented by the word 'tomorrow'. Since yesterday is before tomorrow, with a gap between them, we say that yesterday _precedes_ tomorrow:
67 |
68 | [source.code#relation-yesterday-tomorrow,clojure]
69 | ----
70 | (t/relation (t/yesterday) (t/tomorrow))
71 | ----
72 |
73 | If the two intervals touch each other, in the case of 'today' and 'tomorrow', then we say the first interval (today) _meets_ the second interval (tomorrow).
74 |
75 | [source.code#relation-today-tomorrow,clojure]
76 | ----
77 | (t/relation (t/today) (t/tomorrow))
78 | ----
79 |
80 | To see other possible relations, use the slider in the diagram below to move the top interval along:
81 |
82 | [.interval-relations]
83 | ----
84 | abc
85 | ----
86 | ====
87 |
88 | == Collections
89 |
90 | It is often useful to group intervals into collections and have
91 | functions operate on those collections.
92 |
93 | For example, you may want to gather together:
94 |
95 | * all the time intervals when you were working last week
96 | * system outages over a given period
97 | * public holidays and weekends this year
98 |
99 | NOTE: Discuss ordered sequences of disjoint intervals.
100 |
101 | == Demonstration
102 |
--------------------------------------------------------------------------------
/docs/cljs.adoc:
--------------------------------------------------------------------------------
1 | == Clojurescript
2 |
3 | Tick versions 0.4.24-alpha and up require minimum Clojurescript version of 1.10.741
4 |
5 | There are extra considerations when using tick with Clojurescript
6 |
7 | Tick uses the https://js-joda.github.io/js-joda/[js-joda] library, which aims to replicate the http://www.threeten.org/threetenbp/[three-ten-backport]
8 | project. JS-Joda is broken down into a core project (what tick depends on) and additional timezone
9 | and locale projects.
10 |
11 | === NPM Setup
12 |
13 | If you are using npm in your build, first add the transitive npm dependencies to your package.json. Assuming you have
14 | tick in your deps.edn, from your project directory, run
15 |
16 | ```
17 | clj -m cljs.main --install-deps
18 | ```
19 |
20 | Now your package.json has the required npm libs added. Shadow actually does this for you, if you are using that.
21 |
22 | The only other thing to be aware of is if your build tool supports
23 | `:foreign-libs` (Shadow doesn't) then you should exclude the transitive cljsjs dependencies.
24 | https://clojurescript.org/reference/dependencies#cljsjs[The Clojurescript site] provides more info.
25 |
26 | Note: For tick versions 0.4.23-alpha and earlier, to use tick on shadow follow https://github.com/henryw374/tick-on-shadow-cljs-demo[this demo]
27 |
28 | === Timezones
29 |
30 | If you want to work with timezones, something like this, for example:
31 |
32 | ----
33 | (tick/zone "Europe/London")
34 | ----
35 |
36 | add the following require:
37 |
38 | ----
39 | [tick.timezone]
40 | ----
41 |
42 | Note that this is pulling in all of the history of timezones as well. If you don't need historic data and you
43 | want to reduce build size, js-joda provides pre-build packages for just the more recent data.
44 |
45 | === Formatting
46 |
47 | If you want to create custom formatters from patterns, such as "dd MMM yyyy", add this require:
48 |
49 | ----
50 | [tick.locale-en-us]
51 | ----
52 |
53 | ==== Reducing Tick Build Size
54 |
55 | The extra requires for timezones and locales are have been done that way to allow a smaller payload, when the extra
56 | libraries are not being used.
57 |
58 | Minified, gzipped js-joda (what gets pulled in if you use anything of tick) is around 43k. It could be possible to take advantage
59 | of tree-shaking with JSJoda, see https://github.com/juxt/tick/issues/33[this ticket] for discussion.
60 |
61 | Timezone is an extra 26k, and Locale (just en-US) is an extra 45k
62 |
63 | The js-joda timezone dependency contains the timezone database, containing mappings between zone
64 | names, their offsets from UTC, and daylight savings(DST) data.
65 |
66 | Locale data is needed for custom date formatters which need particular symbols, such as M for month.
67 | Due to the size and complexity of using the js-joda-locale, the authors of js-joda-locale have created
68 | https://github.com/js-joda/js-joda-locale#use-prebuilt-locale-packages[prebuilt locale packages], for specific
69 | locales. en-US is one which is currently packaged for cljs and can be used as suggested above.
70 |
71 | === OffsetTime and OffsetDateTime
72 |
73 | OffsetTime is currently missing from JS-Joda (see
74 | https://github.com/js-joda/js-joda/issues/240[JS-Joda issue 240]). For now, tick uses LocalTime
75 | as the implementation which is not ideal.
76 |
77 | OffsetDateTime is also missing but ZonedDateTime has the same functionality so this shouldn't be a problem.
78 |
--------------------------------------------------------------------------------
/README.adoc:
--------------------------------------------------------------------------------
1 | = tick
2 |
3 | A Clojure(Script) library for dealing with time. Intended as a
4 | replacement for clj-time.
5 |
6 | Based on Java 8 time (on the JVM) and js-joda (on JavaScript
7 | runtimes).
8 |
9 | [source,clojure]
10 | ----
11 | (require '[tick.alpha.api :as t])
12 |
13 | ;; Get the current time
14 | (t/now)
15 | ----
16 |
17 | See https://www.youtube.com/watch?v=UFuL-ZDoB2U[Henry Widd's talk at Clojure/North 2019] for some background
18 |
19 | == Docs
20 |
21 | http://juxt.pro/tick/docs/index.html[Tick Documentation]
22 |
23 | == Status
24 |
25 | Alpha: Ready to use with the caveat that the API might still undergo
26 | minor changes.
27 |
28 | == Install
29 |
30 | Get the latest from https://clojars.org/tick[Clojars] and
31 | add to your `project.clj`, `build.boot` or `deps.edn`.
32 |
33 | Tick versions 0.4.24-alpha and up require minimum Clojurescript version of 1.10.741
34 |
35 | There are some extra considerations when using tick from Clojurescript, see file `docs/cljs.adoc` in this repo.
36 |
37 | Here is a one-liner to drop into a node repl with tick:
38 |
39 | ---
40 | clj -Sdeps '{:deps {org.clojure/clojurescript {:mvn/version "1.10.741" } tick {:mvn/version "0.4.24-alpha"} }}' -m cljs.main -re node --repl
41 |
42 | ---
43 |
44 | == Development
45 |
46 | image:https://circleci.com/gh/juxt/tick/tree/master.svg?style=svg["CircleCI", link="https://circleci.com/gh/juxt/tick/tree/master"]
47 |
48 | === Develop The Documentation Site
49 |
50 | Build the Cljs
51 | ---
52 | make dev-docs-cljs
53 | ---
54 |
55 | Build the html
56 | ---
57 | make docs/index.html
58 | ---
59 |
60 | Serve the docs directory and navigate to it in a browser
61 |
62 | === Develop Tick
63 |
64 | REPL with nREPL server
65 |
66 | ----
67 | make nrepl
68 | ----
69 |
70 | connect to the printed port
71 |
72 | Once in, start cljs build with:
73 |
74 | ---
75 | (figwheel-start!)
76 | ---
77 |
78 | Run tests with:
79 |
80 | ----
81 | make test
82 | ----
83 |
84 | == Documentation
85 |
86 | - https://juxt.github.io/tick[Generated API docs]
87 |
88 | == Acknowledgements
89 |
90 | Tick is based on the same original idea as
91 | https://github.com/jarohen/chime[Chime]. The motivation is to be
92 | able to view timelines of remaining times while the schedule is
93 | running. Thanks to James Henderson for his work on Chime.
94 |
95 | In particular, special credit to Eric Evans for discovering Allen's
96 | interval algebra and pointing out its potential usefulness,
97 | demonstrating a working implementation of Allen's ideas in
98 | link:https://github.com/domainlanguage/time-count[his Clojure library].
99 |
100 | Thanks also to my esteemed colleagues Patrik Kårlin for his redesign of
101 | the interval constructor function, and Henry Widd for porting to cljc.
102 |
103 | == References
104 |
105 | * https://github.com/dm3/clojure.java-time
106 | * https://clojuresync.com/emily-ashley/
107 | * https://github.com/aphyr/tea-time
108 | * https://github.com/sunng87/rigui
109 |
110 | == Copyright & License
111 |
112 | The MIT License (MIT)
113 |
114 | Copyright © 2016-2018 JUXT LTD.
115 |
116 | 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:
117 |
118 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
119 |
120 | 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.
121 |
--------------------------------------------------------------------------------
/deps.edn:
--------------------------------------------------------------------------------
1 | {:paths ["src" "resources"]
2 | :deps
3 | {
4 | cljc.java-time {:mvn/version "0.1.9"}
5 | cljsjs/js-joda-timezone {:mvn/version "2.2.0-0"}
6 | cljsjs/js-joda-locale-en-us {:mvn/version "3.1.1-1"}
7 | time-literals {:mvn/version "0.1.3" :exclusions [cljs.java-time]}}
8 |
9 | :aliases
10 | {:dev
11 | {:extra-deps
12 | {com.bhauman/figwheel-main {:mvn/version "0.1.9"}
13 | com.bhauman/cljs-test-display {:mvn/version "0.1.1"}
14 | org.clojure/clojurescript {:mvn/version "1.10.764"}
15 | org.clojure/data.xml {:mvn/version "0.2.0-alpha5"}
16 | org.clojure/tools.namespace {:mvn/version "0.2.11"}
17 | org.apache.xmlgraphics/batik-swing {:mvn/version "1.9"}
18 | io.aviso/pretty {:mvn/version "0.1.34"}
19 | spyscope {:mvn/version "0.1.6"}
20 | fipp {:mvn/version "0.6.12"}}
21 |
22 | :extra-paths ["dev/src" "test"]
23 | :jvm-opts ["-Dclojure.spec.compile-asserts=true"]}
24 | :test-chrome {:extra-paths ["test" "cljs-test-runner-out/gen"]
25 | :extra-deps {olical/cljs-test-runner {:mvn/version "3.7.0" :exclusions [org.clojure/clojurescript]}
26 | org.clojure/clojurescript {:mvn/version "1.10.764"}}
27 | :main-opts ["-m" "cljs-test-runner.main" "-c" "test/cljs-test-opts.edn -x chrome-headless"]}
28 | ; this is node using foreign-libs.
29 | ; although not recommended, whilst tick depends on cljsjs, this should 'just work'
30 | :test-node {:extra-paths ["test" "cljs-test-runner-out/gen"]
31 | :extra-deps {olical/cljs-test-runner {:mvn/version "3.7.0" :exclusions [org.clojure/clojurescript]}
32 | org.clojure/clojurescript {:mvn/version "1.10.764"}}
33 | :main-opts ["-m" "cljs-test-runner.main" "-x node"]}
34 | :docs-index {:jvm-opts ["-Xmx500M"]
35 | :extra-paths ["docs/src"]
36 | :extra-deps {reagent {:mvn/version "0.8.1"}
37 | com.bhauman/figwheel-main {:mvn/version "0.2.6"}
38 | org.clojure/clojurescript {:mvn/version "1.10.764"}}
39 | :main-opts ["-m" "figwheel.main" "--build" "docs"]
40 | }
41 | :test {:extra-paths ["test"]
42 | :extra-deps {
43 | com.cognitect/test-runner {:git/url "https://github.com/cognitect-labs/test-runner.git"
44 | :sha "028a6d41ac9ac5d5c405dfc38e4da6b4cc1255d5"}}
45 | :main-opts ["-m" "cognitect.test-runner"]}
46 | :dev-nrepl {:extra-deps {nrepl/nrepl {:mvn/version "0.6.0"}
47 | cider/piggieback {:mvn/version "0.4.0"}}}
48 | :emacs-nrepl {:jvm-opts ["-Dnrepl.load=true"]
49 | :extra-paths ["aliases/nrepl"]
50 | :extra-deps
51 | {cider/cider-nrepl {:mvn/version "0.17.0"}
52 | ;;refactor-nrepl {:mvn/version "2.3.1"}
53 | com.cemerick/piggieback {:mvn/version "0.2.2"}
54 | org.clojure/tools.nrepl {:mvn/version "0.2.12"}}}
55 |
56 | :dev-rebel {:extra-paths ["aliases/rebel"]
57 | :extra-deps {com.bhauman/rebel-readline {:mvn/version "0.1.1"}
58 | com.bhauman/rebel-readline-cljs {:mvn/version "0.1.4"}
59 | io.aviso/pretty {:mvn/version "0.1.34"}}
60 | :main-opts ["-m" "tick.rebel.main"]}}}
61 |
--------------------------------------------------------------------------------
/generate_docs:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Keep a separate branch of generated API docs.
4 | #
5 | # This script generates API documentation, commits it to a separate branch, and
6 | # pushes it upstream. It does this without actually checking out the branch,
7 | # using a separate working tree directory, so without any disruption to your
8 | # current working tree. You can have local file modifications, but the git index
9 | # (staging area) must be clean.
10 |
11 | ############################################################
12 | # These variables can all be overridden from the command line,
13 | # e.g. AUTODOC_REMOTE=plexus ./generate_docs
14 |
15 | # The git remote to fetch and push to. Also used to find the parent commit.
16 | AUTODOC_REMOTE=${AUTODOC_REMOTE:-"origin"}
17 |
18 | # Branch name to commit and push to
19 | AUTODOC_BRANCH=${AUTODOC_BRANCH:-"gh-pages"}
20 |
21 | # Command that generates the API docs
22 | #AUTODOC_CMD=${AUTODOC_CMD:-"lein with-profile +codox codox"}
23 | #AUTODOC_CMD=${AUTODOC_CMD:-"boot codox -s src -n my-project -o gh-pages target"}
24 |
25 | # Working tree directory. The output of $AUTODOC_CMD must end up in this directory.
26 | AUTODOC_DIR=${AUTODOC_DIR:-"gh-pages"}
27 |
28 | ############################################################
29 |
30 | function echo_info() {
31 | echo -en "[\033[0;32mautodoc\033[0m] "
32 | echo $*
33 | }
34 |
35 | function echo_error() {
36 | echo -en "[\033[0;31mautodoc\033[0m] "
37 | echo $*
38 | }
39 |
40 | if [[ -z "$AUTODOC_CMD" ]]; then
41 | echo_error "Please specify a AUTODOC_CMD, e.g. lein codox"
42 | exit 1
43 | fi
44 |
45 | if ! git diff-index --quiet --cached HEAD ; then
46 | echo_error "Git index isn't clean. Make sure you have no staged changes. (try 'git reset .')"
47 | exit 1
48 | fi
49 |
50 | VERSION=0019
51 |
52 | echo "//======================================\\\\"
53 | echo "|| AUTODOC v${VERSION} ||"
54 | echo "\\\\======================================//"
55 |
56 | MESSAGE="Updating docs based on $(git rev-parse --abbrev-ref HEAD) $(git rev-parse HEAD)
57 |
58 | Ran: $AUTODOC_CMD
59 | "
60 |
61 | if [[ ! -z "$(git status --porcelain)" ]]; then
62 | MESSAGE="$MESSAGE
63 | Repo not clean.
64 |
65 | Status:
66 | $(git status --short)
67 |
68 | Diff:
69 | $(git diff)"
70 | fi
71 |
72 | # Fetch the remote, we don't care about local branches, only about what's
73 | # currently on the remote
74 | git fetch $AUTODOC_REMOTE
75 |
76 | # Start from a clean slate, we only commit the new output of AUTODOC_CMD, nothing else.
77 | rm -rf $AUTODOC_DIR
78 | mkdir -p $AUTODOC_DIR
79 |
80 | echo_info "Generating docs"
81 | echo $AUTODOC_CMD | bash
82 |
83 | AUTODOC_RESULT=$?
84 |
85 | if [[ ! $AUTODOC_RESULT -eq 0 ]]; then
86 | echo_error "The command '${AUTODOC_CMD}' returned a non-zero exit status (${AUTODOC_RESULT}), giving up."
87 | exit $AUTODOC_RESULT
88 | fi
89 |
90 | if [[ $(find $AUTODOC_DIR -maxdepth 0 -type d -empty 2>/dev/null) ]]; then
91 | echo_error "The command '$AUTODOC_CMD' created no output in '$AUTODOC_DIR', giving up"
92 | exit 1
93 | fi
94 |
95 | # The full output of AUTODOC_CMD is added to the git index, staged to become a new
96 | # file tree+commit
97 | echo_info "Adding file to git index"
98 | git --work-tree=$AUTODOC_DIR add -A
99 |
100 | # Create a git tree object with the exact contents of $AUTODOC_DIR (the output of
101 | # the AUTODOC_CMD), this will be file tree of the new commit that's being created.
102 | TREE=`git write-tree`
103 | echo_info "Created git tree $TREE"
104 |
105 | # Create the new commit, either with the previous remote HEAD as parent, or as a
106 | # new orphan commit
107 | if git show-ref --quiet --verify "refs/remotes/${AUTODOC_REMOTE}/${AUTODOC_BRANCH}" ; then
108 | PARENT=`git rev-parse ${AUTODOC_REMOTE}/${AUTODOC_BRANCH}`
109 | echo "Creating commit with parent refs/remotes/${AUTODOC_REMOTE}/${AUTODOC_BRANCH} ${PARENT}"
110 | COMMIT=$(git commit-tree -p $PARENT $TREE -m "$MESSAGE")
111 | else
112 | echo "Creating first commit of the branch"
113 | COMMIT=$(git commit-tree $TREE -m "$MESSAGE")
114 | fi
115 |
116 | echo_info "Pushing $COMMIT to $AUTODOC_BRANCH"
117 |
118 | # Rest the index, commit-tree doesn't do that by itself. If we don't do this
119 | # `git status` or `git diff` will look *very* weird.
120 | git reset .
121 |
122 | # Push the newly created commit to remote
123 | if [[ ! -z "$PARENT" ]] && [[ $(git rev-parse ${COMMIT}^{tree}) == $(git rev-parse refs/remotes/$AUTODOC_REMOTE/$AUTODOC_BRANCH^{tree} ) ]] ; then
124 | echo_error "WARNING: No changes in documentation output from previous commit. Not pushing to ${AUTODOC_BRANCH}"
125 | else
126 | git push $AUTODOC_REMOTE $COMMIT:refs/heads/$AUTODOC_BRANCH
127 | # Make sure our local remotes are up to date.
128 | git fetch
129 | # Show what happened, you should see a little stat diff here of the changes
130 | echo
131 | git log -1 --stat $AUTODOC_REMOTE/$AUTODOC_BRANCH
132 | fi
133 |
--------------------------------------------------------------------------------
/dev/resources/mycal.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
128 |
--------------------------------------------------------------------------------
/docs/intro.adoc:
--------------------------------------------------------------------------------
1 | = Introduction
2 |
3 | [quote, Douglas Adams, The Hitchhiker's Guide to the Galaxy]
4 | ____
5 | Time is an illusion. Lunchtime doubly so.
6 | ____
7 |
8 | _Tick_ is a comprehensive Clojure(Script) library designed to make it
9 | easier to write programs that involve time and date calculations:
10 |
11 | * Functions to manipulating time, easily and succinctly (stable)
12 | * Powerful functions for slicing and dicing time intervals (stable)
13 | * Implementation of link:https://en.wikipedia.org/wiki/Allen%27s_interval_algebra[Allen's interval algebra] (stable)
14 | * Support for iCalendar serialization (work-in-progress)
15 | * Scheduling (work-in-progress)
16 |
17 | In many business domains, dates are as fundamental as numbers and
18 | strings. It's often desirable to have date-heavy business logic
19 | portable across platforms. _Tick_ supports both Clojure and
20 | ClojureScript, with an identical API.
21 |
22 | Tick is implemented using the api of `java.time` and an understanding of the https://docs.oracle.com/javase/tutorial/datetime/iso/overview.html[concepts behind java.time] will be very useful when working with tick,
23 | because tick entities are java.time entities (Instant, LocalTime etc). Where tick doesn't provide the api you need,
24 | you can look at the java.time api to see if there alternatives. If you cannot find the help you need in the tick documentation, it
25 | is quite likely that someone will have had the same query and had it resolved on https://stackoverflow.com/questions/tagged/java-time[Stack Overflow].
26 |
27 | == Status
28 |
29 | _Tick_ is currently in _alpha_ status. By _alpha_, we mean that the
30 | library's API may change in future. The quality of _tick_ is deemed
31 | adequate for real-world use but do let us know if you come across
32 | any unexpected behaviour and bugs.
33 |
34 | == License
35 |
36 | _Tick_ is copyrighted by JUXT LTD. and licensed as free software under
37 | the open-source MIT License.
38 |
39 | ....
40 | The MIT License (MIT)
41 |
42 | Copyright © 2016-2018 JUXT LTD.
43 |
44 | 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:
45 |
46 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
47 |
48 | 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.
49 | ....
50 |
51 | == Comparison to other time libraries
52 |
53 | === Java 8 time
54 |
55 | Java 8's link:http://www.oracle.com/technetwork/articles/java/jf14-date-time-2125367.html[`java.time`] API is both influenced by, and an improvement on,
56 | Joda Time.
57 |
58 | Unlike older JDK dates and calendars, instances in
59 | `java.time` are immutable so can be considered values in Clojure. For this reason, there is no reason to wrap these values. Consequently, there is full interoperability between _tick_ and `java.time`. Where _tick_ does not provide a part of java.time's functionality, `java.time` can be called directly.
60 |
61 | CAUTION: Because _tick_ is built on `java.time`, Clojure programs must run on Java 8 or higher.
62 |
63 | === clj-time and cljs-time
64 |
65 | Most Clojure applications use `clj-time` which is based on Joda
66 | Time. However, `cljs-time` objects are mutable goog.date objects which in turn wrap
67 | JavaScript Date objects.
68 |
69 | This works OK as a proxy for Instant, but is not a great foundation
70 | for local dates etc.
71 |
72 | The author of cljs-time, Andrew McVeigh, has said he would ideally
73 | move `cljs-time` off `goog.date` but is unlikely to do so at this
74 | point. For one thing, there could be more than a few current users
75 | relying on the JS Date nature of the cljs-time objects.
76 |
77 | Taking a fresh look at the date/time landscape, we now have java.time (JSR-310)
78 | and implementations in both Java and Javascript and so it is possible
79 | to create _tick_, which combines the excellent JSR-310 with an
80 | expressive, cross-platform Clojure(Script) API.
81 |
82 | For some use cases it is possible to write cross-platform code with clj/s-time, conditionally requiring clj-time
83 | or cljs-time in a cljc file. In our experience though, the fact that cljs-time doesn't have complete fidelity
84 | with clj-time often comes to be a problem.
85 |
86 | === Dropping to java.time
87 |
88 | Tick depends on https://github.com/henryw374/cljc.java-time[cljc.java-time] which is the easiest way to access the full
89 | java.time api underneath tick.
90 |
91 | === Quartz
92 |
93 | See https://dzone.com/articles/why-you-shouldnt-use-quartz
94 |
--------------------------------------------------------------------------------
/docs/cookbook/countdown.adoc:
--------------------------------------------------------------------------------
1 | == Countdown timers
2 |
3 | [.lead]
4 | Creating a countdown timer greatly depends on the length of time being counted and the accuracy required.
5 |
6 |
7 | For a simple timer, usually only hours minutes and seconds are required:
8 |
9 | ----
10 | (defn countdown-HH-mm-ss
11 | [end-time]
12 | (let [duration (tick/duration
13 | {:tick/beginning (tick/instant)
14 | :tick/end end-time})
15 | hours (tick/hours duration)
16 | minutes (tick/minutes (tick/- duration
17 | (tick/new-duration hours :hours)))
18 | seconds (tick/seconds (tick/- duration
19 | (tick/new-duration minutes :minutes)
20 | (tick/new-duration hours :hours)))]
21 | (if (tick/< (tick/instant) end-time)
22 | (format "%02d:%02d:%02d"
23 | hours minutes seconds)
24 | "Time's up!")))
25 | ----
26 |
27 |
28 | For longer durations, counting to high precision is unnecessary. If we are counting down the weeks, knowing how many seconds
29 | remain is for the most part meaningless.
30 |
31 | ----
32 | (defn countdown-weeks
33 | [end-time]
34 | (let [duration (tick/duration
35 | {:tick/beginning (tick/instant)
36 | :tick/end end-time})
37 | weeks (long (tick/divide duration (tick/new-duration 7 :days)))
38 | days (t/days (t/- duration
39 | (t/new-duration (* weeks 7) :days)))
40 | hours (tick/hours (tick/- duration
41 | (t/new-duration (+ days (* weeks 7)) :days)))]
42 | (if (tick/< (tick/instant) end-time)
43 | (format "%d weeks, %d days, %d hours"
44 | weeks days hours)
45 | "Time's up!")))
46 |
47 | ----
48 |
49 | If you do not know the units of time that are going to be counted down, you may require a more general countdown function.
50 |
51 | ----
52 | (defn countdown-generic
53 | "Gives a map of the countdown with units of time as keys."
54 | [end-time]
55 | (let [duration (tick/duration
56 | {:tick/beginning (tick/instant)
57 | :tick/end end-time})
58 | weeks (long (tick/divide duration (tick/new-duration 7 :days)))
59 | days (t/days (t/- duration
60 | (t/new-duration (* weeks 7) :days)))
61 | hours (tick/hours (tick/- duration
62 | (t/new-duration (+ days (* weeks 7)) :days)))
63 | minutes (tick/minutes (tick/- duration
64 | (t/new-duration (+ days (* weeks 7)) :days)
65 | (t/new-duration hours :hours)))
66 | seconds (tick/seconds (tick/- duration
67 | (t/new-duration (+ days (* weeks 7)) :days)
68 | (t/new-duration hours :hours)
69 | (t/new-duration minutes :minutes)))
70 | millis (tick/millis (tick/- duration
71 | (t/new-duration (+ days (* weeks 7)) :days)
72 | (t/new-duration hours :hours)
73 | (t/new-duration minutes :minutes)
74 | (t/new-duration seconds :seconds)))]
75 | (if (tick/< (tick/instant) end-time)
76 | {:counting true
77 | :weeks weeks
78 | :days days
79 | :hours hours
80 | :minutes minutes
81 | :seconds seconds
82 | :milliseconds millis}
83 | {:counting false})))
84 | ----
85 |
86 | It may be required that the time _since_ an event is calculated. In this can be done in a very similar way to counting down:
87 |
88 | ----
89 | (defn count-up
90 | "Gives the time since an event in the most appropriate units of time"
91 | [event]
92 | (let [duration (tick/duration
93 | {:tick/beginning event
94 | :tick/end (tick/instant)})
95 | years (long (tick/divide duration (tick/new-duration 365 :days)))
96 | months (long (tick/divide duration (tick/new-duration (/ 365 12) :days)))
97 | weeks (long (tick/divide duration (tick/new-duration 7 :days)))]
98 | (cond
99 | (> (t/days duration) 365)
100 | (format "%d years" years)
101 |
102 | (and (<= (t/days duration) 365) (> (t/days duration) (/ 365 12)))
103 | (format "%d months" months)
104 |
105 | (and (<= (t/days duration) (/ 365 12)) (> (t/days duration) 7))
106 | (format "%d weeks" weeks)
107 |
108 | (and (<= (t/days duration) 7) (> (t/days duration) 1))
109 | (format "%d days" (t/days duration))
110 |
111 | (and (<= (t/days duration) 1) (> (t/hours duration) 1))
112 | (format "%d hours %d" (t/hours duration))
113 |
114 | (and (<= (t/hours duration) 1) (> (t/minutes duration) 1))
115 | (format "%d minutes %d" (t/minutes duration))
116 |
117 | (and (<= (t/minutes duration) 1) (> (t/seconds duration) 1))
118 | (format "%d seconds" (t/seconds duration))
119 |
120 | (tick/< (tick/instant) event)
121 | "Event hasn't happened yet")))
122 | ----
123 |
124 | CAUTION: These timers have lower accuracy at higher precisions - they do not account for leap seconds or years.
--------------------------------------------------------------------------------
/src/tick/deprecated/cal.clj:
--------------------------------------------------------------------------------
1 | ;; Copyright © 2016-2017, JUXT LTD.
2 |
3 | (ns tick.deprecated.cal
4 | (:require
5 | [clojure.spec.alpha :as s]
6 | [tick.core :as t])
7 | (:import
8 | [java.time Clock ZoneId Instant Duration DayOfWeek Month ZonedDateTime LocalDate YearMonth Month MonthDay]
9 | [java.time.temporal ChronoUnit]))
10 |
11 | (s/def ::year int?)
12 | (s/def ::name string?)
13 | (s/def ::date #(instance? LocalDate %))
14 | (s/def ::substitute-day boolean?)
15 | (s/def ::holiday (s/keys :req [::name ::date]
16 | :opt [::substitute-day]))
17 |
18 | (defn day-of-week
19 | "Return the day of the week for a given ZonedDateTime"
20 | [dt]
21 | (.getDayOfWeek dt))
22 |
23 | (defn weekend?
24 | "Is the ZonedDateTime during the weekend?"
25 | [dt]
26 | (#{DayOfWeek/SATURDAY DayOfWeek/SUNDAY} (day-of-week dt)))
27 |
28 | (defn past? [now]
29 | (fn [d] (.isBefore d now)))
30 |
31 | (defn- first-named-day-from [ld day]
32 | (first (drop-while #(not= (day-of-week %) day) (t/range ld))))
33 |
34 | (defn- last-named-day-from [ld day]
35 | (first (drop-while #(not= (day-of-week %) day) (t/range ld nil -1))))
36 |
37 | (defn first-monday-of-month [^YearMonth ym]
38 | (first-named-day-from (.atDay ym 1) DayOfWeek/MONDAY))
39 |
40 | (defn last-monday-of-month [^YearMonth ym]
41 | (last-named-day-from (.atEndOfMonth ym) DayOfWeek/MONDAY))
42 |
43 | (defn first-friday-of-month [^YearMonth ym]
44 | (first-named-day-from (.atDay ym 1) DayOfWeek/FRIDAY))
45 |
46 | (defn last-friday-of-month [^YearMonth ym]
47 | (last-named-day-from (.atEndOfMonth ym) DayOfWeek/FRIDAY))
48 |
49 | (defn holiday
50 | ([name day]
51 | {:name name
52 | :date day})
53 | ([name day hol]
54 | {:name name
55 | :date hol
56 | :substitute-day (and hol (not= day hol))}))
57 |
58 | (s/fdef holiday
59 | :args (s/cat :name ::name :day ::date :hol ::date)
60 | :ret ::holiday)
61 |
62 | (defn new-years-day [year]
63 | (LocalDate/of (t/int (t/year year)) 1 1))
64 |
65 | (defn new-years-day-holiday [year]
66 | (let [day (new-years-day (t/int (t/year year)))
67 | hol (cond-> day (weekend? day) (first-named-day-from DayOfWeek/MONDAY))]
68 | (holiday "New Year's Day" day hol)))
69 |
70 | (defn easter-sunday
71 | "Return a pair containing [month day] of Easter Sunday given the
72 | year. Copyright © 2016 Eivind Waaler. EPL v1.0. From
73 | https://github.com/eivindw/clj-easter-day, using Spencer Jones
74 | formula."
75 | ;; TODO: From what year does this algorithm makes sense from, need
76 | ;; to throw an exception outside this range.
77 | [year]
78 | (let [year (t/int (t/year year))
79 | a (mod year 19)
80 | b (quot year 100)
81 | c (mod year 100)
82 | d (quot b 4)
83 | e (mod b 4)
84 | f (quot (+ b 8) 25)
85 | g (quot (+ (- b f) 1) 3)
86 | h (mod (+ (* 19 a) (- b d g) 15) 30)
87 | i (quot c 4)
88 | k (mod c 4)
89 | l (mod (- (+ 32 (* 2 e) (* 2 i)) h k) 7)
90 | m (quot (+ a (* 11 h) (* 22 l)) 451)
91 | n (quot (+ h (- l (* 7 m)) 114) 31)
92 | p (mod (+ h (- l (* 7 m)) 114) 31)]
93 | (LocalDate/of year n (+ p 1))))
94 |
95 | (defn good-friday [year]
96 | (.minusDays (easter-sunday year) 2))
97 |
98 | (defn good-friday-holiday [year]
99 | (holiday "Good Friday" (good-friday year)))
100 |
101 | (defn easter-monday [year]
102 | (.plusDays (easter-sunday year) 1))
103 |
104 | (defn easter-monday-holiday [year]
105 | (holiday "Easter Monday" (easter-monday year)))
106 |
107 | (defn may-day
108 | ([]
109 | (MonthDay/of Month/MAY 1))
110 | ([year]
111 | (.atMonthDay (t/year year) (may-day))))
112 |
113 | (defn early-may-bank-holiday [year]
114 | (holiday "Early May bank holiday"
115 | (first-named-day-from (may-day (t/year year)) DayOfWeek/MONDAY)))
116 |
117 | (defn spring-bank-holiday [year]
118 | (holiday "Spring bank holiday"
119 | (last-monday-of-month (.atMonth (t/year year) Month/MAY))))
120 |
121 | (defn summer-bank-holiday [year]
122 | (holiday "Summer bank holiday"
123 | (last-monday-of-month (.atMonth (t/year year) Month/AUGUST))))
124 |
125 | (defn christmas-day
126 | ([]
127 | (MonthDay/of Month/DECEMBER 25))
128 | ([year]
129 | (.atMonthDay (t/year year) (christmas-day))))
130 |
131 | (s/fdef christmas-day
132 | :args (s/cat :year ::year)
133 | :ret ::date)
134 |
135 | (defn christmas-day-holiday [year]
136 | (let [day (christmas-day (t/year year))
137 | hol (cond-> day
138 | (#{DayOfWeek/SATURDAY DayOfWeek/SUNDAY} (.getDayOfWeek day)) (.plusDays 2))]
139 | (holiday "Christmas Day" day hol)))
140 |
141 | (s/fdef christmas-day-holiday
142 | :args (s/cat :year ::year)
143 | :ret ::holiday)
144 |
145 | (defn boxing-day
146 | ([]
147 | (MonthDay/of Month/DECEMBER 26))
148 | ([year]
149 | (.atMonthDay (t/year year) (boxing-day))))
150 |
151 | (s/fdef boxing-day
152 | :args (s/cat :year ::year)
153 | :ret ::date)
154 |
155 | (defn boxing-day-holiday [year]
156 | (let [day (boxing-day (t/int (t/year year)))
157 | hol (cond-> day
158 | (#{DayOfWeek/SATURDAY DayOfWeek/SUNDAY} (.getDayOfWeek day)) (.plusDays 2))]
159 | (holiday "Boxing Day" day hol)))
160 |
161 | (s/fdef boxing-day-holiday
162 | :args (s/cat :year ::year)
163 | :ret ::holiday)
164 |
165 | (def holidays-in-england-and-wales
166 | (juxt new-years-day-holiday
167 | good-friday-holiday
168 | easter-monday-holiday
169 | early-may-bank-holiday
170 | spring-bank-holiday
171 | summer-bank-holiday
172 | christmas-day-holiday
173 | boxing-day-holiday))
174 |
175 | ;; TODO: Scotland
176 |
177 | ;; TODO: Northern Ireland
178 |
179 | ;; TODO: Republic of Ireland
180 |
181 | ;; TODO: Isle of Man
182 |
--------------------------------------------------------------------------------
/docs/src/tick/docs/app.cljs:
--------------------------------------------------------------------------------
1 | (ns tick.docs.app
2 | (:require-macros [tick.docs.app :refer [analyzer-state]])
3 | (:require
4 | [reagent.core :as r]
5 | [tick.alpha.api :as t]
6 | [tick.timezone]
7 | [clojure.string :refer [lower-case capitalize]]
8 | [cljs.js :refer [empty-state eval js-eval eval-str]]
9 | [cljs.tools.reader :refer [read-string]]
10 | [cljs.env :as env]))
11 |
12 | (def state (cljs.js/empty-state))
13 |
14 | (defn eval-code [source cb label]
15 | (cljs.js/eval-str
16 | state
17 | ; don't know how to do namespacing
18 | (clojure.string/replace source "t/" "tick.alpha.api/")
19 | (str "[" label "]")
20 | {:eval cljs.js/js-eval :context :expr}
21 | cb))
22 |
23 | (defn load-library-analysis-cache! []
24 | (cljs.js/load-analysis-cache! state 'tick.alpha.api (analyzer-state 'tick.alpha.api))
25 | ;(cljs.js/load-analysis-cache! state 't (analyzer-state 'tick.alpha.api))
26 | (cljs.js/load-analysis-cache! state 'tick.timezone (analyzer-state 'tick.timezone))
27 | nil)
28 |
29 | (defn day-midnight-today []
30 | (t/day-of-week (t/end (t/bounds (t/today)))))
31 |
32 | (defn day-midnight-tomorrow []
33 | (t/day-of-week (t/end (t/bounds (t/tomorrow)))))
34 |
35 | (defn two-days-from-today []
36 | (str "on " (capitalize (str (day-midnight-tomorrow))) " morning"))
37 |
38 | (defn button [label cb]
39 | [:button
40 | {:key label
41 | :onClick cb}
42 | label])
43 |
44 | (defn code-component [code result label]
45 | [:div
46 | [:div.content
47 | [:pre.highlight
48 | [:code.language-clojure {:data-lang "clojure"} code (when-let [v @result] (str " => " v))]
49 | [:div.code-buttons
50 | (list
51 | (button "Eval" (fn [ev]
52 | (eval-code code
53 | (fn [x]
54 | (println (pr-str x))
55 | (reset! result (str (:value x))))
56 | label)))
57 | (button "Clr" (fn [ev]
58 | (reset! result nil)
59 | )))]]]])
60 |
61 | (defn interval-relations [config *value]
62 | (let [value (js/parseInt @*value 10) ; sometimes we get passed strings!
63 | width 720
64 | x-cells 10
65 | cell-width (/ width x-cells)
66 | fixed-block-width-in-cells 4
67 | fixed-block-width (* fixed-block-width-in-cells cell-width)
68 | block-width-in-cells 2
69 | block-width (* block-width-in-cells cell-width)
70 | min 0
71 | max (- x-cells block-width-in-cells)
72 |
73 | now (t/now)
74 | ->time #(t/+ now (t/new-duration (inc %) :seconds))
75 |
76 | ival1 (t/new-interval
77 | (->time value)
78 | (->time (+ value block-width-in-cells)))
79 |
80 | ival2 (t/new-interval
81 | (->time (- (/ x-cells 2) (/ fixed-block-width-in-cells 2)))
82 | (->time (+ (/ x-cells 2) (/ fixed-block-width-in-cells 2))))]
83 |
84 | [:div.diagram
85 | [:div
86 | [:p
87 | [:input {:style {:width "100%"}
88 | :type :range
89 | :value value
90 | :min min :max max
91 | :onChange (fn [ev]
92 | (reset! *value (.-value (.-target ev))))}]]]
93 |
94 | [:svg {:viewBox [0 0 width 40]}
95 | [:rect
96 | {:x (* cell-width value) :y 10 :width block-width :height 8 :fill "orange"}]
97 |
98 | ;; fixed
99 | [:rect
100 | {:x (- (/ width 2) (/ fixed-block-width 2)) :y 30 :width fixed-block-width :height 8 :fill "#444"}]]
101 |
102 | (letfn [(f [rel] (case rel
103 | :precedes "precedes"
104 | :meets "meets"
105 | :starts "starts"
106 | :during "is during"
107 | :finishes "finishes"
108 | :overlaps "overlaps"
109 | :contains "contains"
110 | :overlapped-by "is overlapped by"
111 | :started-by "is started by"
112 | :finished-by "is finished by"
113 | :met-by "is met by"
114 | :preceded-by "is preceded by"
115 | ))]
116 | [:div
117 | [:p "The higher interval "
118 | [:em (f (t/relation ival1 ival2))]
119 | " the lower interval, whereas the lower interval "
120 | [:em (f (t/relation ival2 ival1))]
121 | " the higher interval."]
122 | [:p "Relation between higher and lower interval: " [:tt (pr-str (t/relation ival1 ival2))]]
123 | [:p "Relation between lower and higher interval: " [:tt (pr-str (t/relation ival2 ival1))]]])]))
124 |
125 | (defonce code-blocks
126 | (for [el (array-seq (.querySelectorAll js/document ".code"))]
127 | {:el el
128 | :id (.-id el)
129 | :code (.-innerText (.querySelector el "pre"))
130 | :result (r/atom nil)}))
131 |
132 | (defonce interval-relation-diagrams
133 | (for [el (array-seq (.querySelectorAll js/document ".interval-relations"))]
134 | {:el el
135 | :id (.-id el)
136 | :config (.-innerText (.querySelector el "pre"))
137 | :value (r/atom 0)}))
138 |
139 | (defn init []
140 |
141 | ;; Read this and weep: https://github.com/arichiardi/replumb/commit/339fe2aa39bb794ca34710317b109bf07916de27
142 | (js* "goog.isProvided_ = function(x) { return false; };")
143 |
144 | (.log js/console "Starting up…")
145 |
146 | (load-library-analysis-cache!)
147 |
148 | (r/render [two-days-from-today]
149 | (.getElementById js/document "eval-two-days-from-today"))
150 |
151 | (doseq [{:keys [id el code result]} code-blocks]
152 | (r/render [code-component code result id] el))
153 |
154 | (doseq [{:keys [el config value]} interval-relation-diagrams]
155 | (r/render [interval-relations config value] el)))
156 |
157 | (init)
158 |
--------------------------------------------------------------------------------
/src/tick/deprecated/schedule.clj:
--------------------------------------------------------------------------------
1 | (ns tick.deprecated.schedule
2 | (:import
3 | [java.time Clock ZoneId Instant Duration DayOfWeek Month ZonedDateTime LocalDate]
4 | [java.time.temporal ChronoUnit]
5 | [java.util.concurrent TimeUnit ScheduledThreadPoolExecutor]))
6 |
7 | (defn schedule-next [next-time {:keys [clock executor callback]}]
8 | (when next-time
9 | (let [dly (.until (.instant clock) (:tick/date next-time) ChronoUnit/MILLIS)]
10 | (.schedule executor ^Callable callback dly TimeUnit/MILLISECONDS))))
11 |
12 | (defn callback [state {:keys [clock trigger executor promise] :as opts}]
13 | ;; The time is now. We have been called for a reason. That reason
14 | ;; might be that we have to run a task.
15 | (let [{:keys [timeline due status] :as result}
16 | (swap! state
17 | (fn [st]
18 | (if-not (= :running (:status st))
19 | ;; If we are stopped, paused or done, then we do nothing. We don't
20 | ;; schedule another task. We just exit.
21 | (dissoc st :due)
22 |
23 | (let [[due next-timeline] (split-with #(not (.isAfter (.toInstant (:tick/date %)) (.instant clock))) (:timeline st))]
24 |
25 | (cond-> st
26 | (not (empty? next-timeline)) (assoc :timeline next-timeline)
27 | (empty? next-timeline) (-> (assoc :status :done)
28 | (dissoc :timeline))
29 | due (assoc :due due))))))]
30 |
31 | (when due
32 | (when (and (= status :running) timeline)
33 | (schedule-next (first timeline)
34 | {:clock clock
35 | :executor executor
36 | :callback #(callback state opts)}))
37 |
38 | (doseq [job due]
39 | (.submit executor #(trigger job))))
40 |
41 | (when (= status :done)
42 | (deliver promise :done))))
43 |
44 | (defprotocol ITicker
45 | "A ticker travels across a timeline, usually triggering some action for each time on the timeline."
46 | (start [_ clock] "Start a ticker. If required, deref the result to block until the schedule is complete.")
47 | (pause [_] "If supported by the ticker, pause. Can be resumed.")
48 | (resume [_] "Resume a paused ticker.")
49 | (stop [_] "Stop the ticker. Can be restarted with start.")
50 | (remaining [_] "Return the remaining timeline yet to be visited by the ticker.")
51 | (clock [_] "Return the clock indicating where the ticker is in the timeline."))
52 |
53 | (defrecord SchedulingTicker [trigger timeline executor state promise]
54 | ITicker
55 | (start [_ clock]
56 | (let [{:keys [timeline]} (swap! state assoc
57 | :status :running
58 | :timeline timeline
59 | :clock clock
60 | :executor executor)]
61 | (schedule-next
62 | (first timeline)
63 | {:clock clock
64 | :executor executor
65 | :callback #(callback state {:clock clock :trigger trigger :executor executor :promise promise})}))
66 |
67 | promise)
68 |
69 | (pause [_]
70 | (let [st @state]
71 | (swap! state (fn [st] (-> st (assoc :status :paused))))
72 | :ok))
73 |
74 | (resume [_]
75 | (let [st @state]
76 | (when (= :paused (:status st))
77 | (let [timeline (:timeline st)
78 | executor (:executor st)
79 | clock (:clock st)]
80 | (let [{:keys [timeline]}
81 | (swap! state (fn [st] (-> st (assoc :status :running
82 | ))))]
83 | (schedule-next
84 | (first timeline)
85 | {:clock clock
86 | :executor executor
87 | :callback #(callback state {:clock clock :trigger trigger :executor executor :promise promise})})))
88 | :ok)))
89 |
90 | (stop [_]
91 | (swap! state (fn [s] (-> s (assoc :status :stopped))))
92 | :ok)
93 |
94 | (remaining [_]
95 | (:timeline @state))
96 | (clock [_]
97 | (:clock @state)))
98 |
99 | (defn schedule
100 | "Think of this like map, but applying a function over a timeline. Returns a ticker."
101 | ([trigger timeline]
102 | (schedule trigger timeline {}))
103 | ([trigger timeline {:keys [executor]}]
104 | (map->SchedulingTicker
105 | {:trigger trigger
106 | :timeline timeline
107 | :state (atom {})
108 | :executor (or executor (new ScheduledThreadPoolExecutor 16))
109 | :promise (promise)})))
110 |
111 | (defrecord ImpatientTicker [trigger timeline state executor]
112 | ITicker
113 | (start [this clock]
114 | (swap! state assoc
115 | :status :running
116 | :timeline timeline
117 | :clock clock)
118 | (.submit
119 | executor
120 | ^Runnable
121 | (fn []
122 | (loop [timeline timeline]
123 | (when-let [tick (first timeline)]
124 | (when (= (:status @state) :running)
125 | ;; Advance clock
126 | (swap! state assoc
127 | :clock (Clock/fixed (.toInstant (:tick/date tick)) (.getZone clock))
128 | :timeline (next timeline))
129 | ;; Call trigger
130 | (trigger tick)
131 | (recur (next timeline))))))))
132 |
133 | (pause [this] :unsupported)
134 | (resume [this] :unsupported)
135 | (stop [this]
136 | (swap! state assoc :status :stopped)
137 | :ok)
138 | (remaining [this] (:timeline @state))
139 | (clock [this] (:clock @state)))
140 |
141 | (defn simulate
142 | "Like schedule, but return a ticker that eagerly advances the clock
143 | to the next time in the timeline and serially executes the trigger."
144 | ([trigger timeline]
145 | (simulate trigger timeline {}))
146 | ([trigger timeline {:keys [executor]}]
147 | (map->ImpatientTicker {:trigger trigger
148 | :timeline timeline
149 | :state (atom {})
150 | :executor (or executor (new ScheduledThreadPoolExecutor 1))})))
151 |
--------------------------------------------------------------------------------
/test/tick/deprecated/cal_test.clj:
--------------------------------------------------------------------------------
1 | ;; Copyright © 2016-2017, JUXT LTD.
2 |
3 | (ns tick.deprecated.cal-test
4 | (:require
5 | [clojure.test :refer :all]
6 | [tick.deprecated.cal :refer :all])
7 | (:import
8 | [java.time LocalDate YearMonth DayOfWeek]))
9 |
10 | ;; https://www.gov.uk/bank-holidays
11 | ;; https://en.wikipedia.org/wiki/Bank_holiday
12 |
13 | (deftest ^:deprecated holidays
14 | (is (= {:name "New Year's Day"
15 | :date (LocalDate/parse "2012-01-02")
16 | :substitute-day true}
17 | (new-years-day-holiday 2012)))
18 | (is (= {:name "Good Friday"
19 | :date (LocalDate/parse "2012-04-06")}
20 | (good-friday-holiday 2012)))
21 | (is (= {:name "Easter Monday"
22 | :date (LocalDate/parse "2012-04-09")}
23 | (easter-monday-holiday 2012)))
24 | (is (= {:name "Early May bank holiday"
25 | :date (LocalDate/parse "2012-05-07")}
26 | (early-may-bank-holiday 2012)))
27 |
28 | ;; TODO: Add exceptions from https://en.wikipedia.org/wiki/Bank_holiday
29 | #_(is (= {:name "Spring bank holiday"
30 | :date (LocalDate/parse "2012-06-04")
31 | :substitute-day true}
32 | (spring-bank-holiday 2012)))
33 |
34 | (is (= {:name "Summer bank holiday"
35 | :date (LocalDate/parse "2012-08-27")}
36 | (summer-bank-holiday 2012)))
37 | (is (= {:name "Christmas Day"
38 | :date (LocalDate/parse "2012-12-25")
39 | :substitute-day false}
40 | (christmas-day-holiday 2012)))
41 | (is (= {:name "Boxing Day"
42 | :date (LocalDate/parse "2012-12-26")
43 | :substitute-day false}
44 | (boxing-day-holiday 2012)))
45 |
46 | (is (= {:name "New Year's Day"
47 | :date (LocalDate/parse "2013-01-01")
48 | :substitute-day false}
49 | (new-years-day-holiday 2013)))
50 | (is (= {:name "Good Friday"
51 | :date (LocalDate/parse "2013-03-29")}
52 | (good-friday-holiday 2013)))
53 | (is (= {:name "Easter Monday"
54 | :date (LocalDate/parse "2013-04-01")}
55 | (easter-monday-holiday 2013)))
56 | (is (= {:name "Early May bank holiday"
57 | :date (LocalDate/parse "2013-05-06")}
58 | (early-may-bank-holiday 2013)))
59 | (is (= {:name "Spring bank holiday"
60 | :date (LocalDate/parse "2013-05-27")}
61 | (spring-bank-holiday 2013)))
62 | (is (= {:name "Summer bank holiday"
63 | :date (LocalDate/parse "2013-08-26")}
64 | (summer-bank-holiday 2013)))
65 | (is (= {:name "Christmas Day"
66 | :date (LocalDate/parse "2013-12-25")
67 | :substitute-day false}
68 | (christmas-day-holiday 2013)))
69 | (is (= {:name "Boxing Day"
70 | :date (LocalDate/parse "2013-12-26")
71 | :substitute-day false}
72 | (boxing-day-holiday 2013)))
73 |
74 | (is (= {:name "New Year's Day"
75 | :date (LocalDate/parse "2014-01-01")
76 | :substitute-day false}
77 | (new-years-day-holiday 2014)))
78 | (is (= {:name "Good Friday"
79 | :date (LocalDate/parse "2014-04-18")}
80 | (good-friday-holiday 2014)))
81 | (is (= {:name "Easter Monday"
82 | :date (LocalDate/parse "2014-04-21")}
83 | (easter-monday-holiday 2014)))
84 | (is (= {:name "Early May bank holiday"
85 | :date (LocalDate/parse "2014-05-05")}
86 | (early-may-bank-holiday 2014)))
87 | (is (= {:name "Spring bank holiday"
88 | :date (LocalDate/parse "2014-05-26")}
89 | (spring-bank-holiday 2014)))
90 | (is (= {:name "Summer bank holiday"
91 | :date (LocalDate/parse "2014-08-25")}
92 | (summer-bank-holiday 2014)))
93 | (is (= {:name "Christmas Day"
94 | :date (LocalDate/parse "2014-12-25")
95 | :substitute-day false}
96 | (christmas-day-holiday 2014)))
97 | (is (= {:name "Boxing Day"
98 | :date (LocalDate/parse "2014-12-26")
99 | :substitute-day false}
100 | (boxing-day-holiday 2014)))
101 |
102 | (is (= {:name "New Year's Day"
103 | :date (LocalDate/parse "2015-01-01")
104 | :substitute-day false}
105 | (new-years-day-holiday 2015)))
106 | (is (= {:name "Good Friday"
107 | :date (LocalDate/parse "2015-04-03")}
108 | (good-friday-holiday 2015)))
109 | (is (= {:name "Easter Monday"
110 | :date (LocalDate/parse "2015-04-06")}
111 | (easter-monday-holiday 2015)))
112 | (is (= {:name "Early May bank holiday"
113 | :date (LocalDate/parse "2015-05-04")}
114 | (early-may-bank-holiday 2015)))
115 | (is (= {:name "Spring bank holiday"
116 | :date (LocalDate/parse "2015-05-25")}
117 | (spring-bank-holiday 2015)))
118 | (is (= {:name "Summer bank holiday"
119 | :date (LocalDate/parse "2015-08-31")}
120 | (summer-bank-holiday 2015)))
121 | (is (= {:name "Christmas Day"
122 | :date (LocalDate/parse "2015-12-25")
123 | :substitute-day false}
124 | (christmas-day-holiday 2015)))
125 | (is (= {:name "Boxing Day"
126 | :date (LocalDate/parse "2015-12-28")
127 | :substitute-day true}
128 | (boxing-day-holiday 2015)))
129 |
130 | (is (= {:name "New Year's Day"
131 | :date (LocalDate/parse "2016-01-01")
132 | :substitute-day false}
133 | (new-years-day-holiday 2016)))
134 | (is (= {:name "Good Friday"
135 | :date (LocalDate/parse "2016-03-25")}
136 | (good-friday-holiday 2016)))
137 | (is (= {:name "Easter Monday"
138 | :date (LocalDate/parse "2016-03-28")}
139 | (easter-monday-holiday 2016)))
140 | (is (= {:name "Early May bank holiday"
141 | :date (LocalDate/parse "2016-05-02")}
142 | (early-may-bank-holiday 2016)))
143 | (is (= {:name "Spring bank holiday"
144 | :date (LocalDate/parse "2016-05-30")}
145 | (spring-bank-holiday 2016)))
146 | (is (= {:name "Summer bank holiday"
147 | :date (LocalDate/parse "2016-08-29")}
148 | (summer-bank-holiday 2016)))
149 | (is (= {:name "Boxing Day"
150 | :date (LocalDate/parse "2016-12-26")
151 | :substitute-day false}
152 | (boxing-day-holiday 2016)))
153 | (is (= {:name "Christmas Day"
154 | :date (LocalDate/parse "2016-12-27")
155 | :substitute-day true}
156 | (christmas-day-holiday 2016)))
157 |
158 | (is (= {:name "New Year's Day"
159 | :date (LocalDate/parse "2017-01-02")
160 | :substitute-day true}
161 | (new-years-day-holiday 2017)))
162 | (is (= {:name "Good Friday"
163 | :date (LocalDate/parse "2017-04-14")}
164 | (good-friday-holiday 2017)))
165 | (is (= {:name "Easter Monday"
166 | :date (LocalDate/parse "2017-04-17")}
167 | (easter-monday-holiday 2017)))
168 | (is (= {:name "Early May bank holiday"
169 | :date (LocalDate/parse "2017-05-01")}
170 | (early-may-bank-holiday 2017))))
171 |
--------------------------------------------------------------------------------
/dev/resources/calendar-33104.svg:
--------------------------------------------------------------------------------
1 |
2 |
30 |
--------------------------------------------------------------------------------
/docs/cookbook/time.adoc:
--------------------------------------------------------------------------------
1 | == Times & dates
2 |
3 | Tick is flexible with the way in which times and dates are created; ergo,
4 | increasing efficiency.
5 | Times and dates can be easily stripped down to smaller modules of time,
6 | likewise they can be built up into complete instants.
7 |
8 | === Create time
9 |
10 | ====
11 | A specific time can be produced in multiple ways with varying degrees of precision:
12 | [source.code,clojure]
13 | ----
14 | (t/time "12:34")
15 | ----
16 |
17 | [source.code,clojure]
18 | ----
19 | (t/time "12:34:56.789")
20 | ----
21 |
22 | [source.code,clojure]
23 | ----
24 | (t/new-time 12 34)
25 | ----
26 |
27 | [source.code,clojure]
28 | ----
29 | (t/new-time 12 34 56 789000000)
30 | ----
31 | ====
32 |
33 | === Get the time
34 |
35 | ====
36 | To get the current time:
37 |
38 | [source.code,clojure]
39 | ----
40 | (t/time)
41 | ----
42 |
43 | [source.code,clojure]
44 | ----
45 | (t/new-time)
46 | ----
47 | ====
48 |
49 | ====
50 | Or the time from an instant:
51 |
52 | [source.code,clojure]
53 | ----
54 | (t/time (t/instant "1999-12-31T23:59:59"))
55 | ----
56 | ====
57 |
58 | ====
59 | Get the current time in another time-zone:
60 |
61 | [source.code,clojure]
62 | ----
63 | (t/time (t/in (t/now) "Australia/Darwin"))
64 | ----
65 | ====
66 |
67 | ====
68 | Get a specific unit of time:
69 | [source.code,clojure]
70 | ----
71 | (t/hour (t/instant "1999-12-31T23:59:59"))
72 | ----
73 | [source.code,clojure]
74 | ----
75 | (t/minute (t/instant "1999-12-31T23:59:59"))
76 | ----
77 | [source.code,clojure]
78 | ----
79 | (t/second (t/instant "1999-12-31T23:59:59"))
80 | ----
81 | ====
82 |
83 | === Create a date
84 | ====
85 | Creating dates is done in much the same way as creating time.
86 | [source.code,clojure]
87 | ----
88 | (t/date "2000-01-01")
89 | ----
90 | [source.code,clojure]
91 | ----
92 | (t/new-date 2000 01 01)
93 | ----
94 | ====
95 |
96 | === Get the date
97 | ====
98 | To get the current date:
99 |
100 | [source.code,clojure]
101 | ----
102 | (t/date)
103 | ----
104 | [source.code,clojure]
105 | ----
106 | (t/new-date)
107 | ----
108 | ====
109 |
110 | ====
111 | Or the date from an instant:
112 | [source.code,clojure]
113 | ----
114 | (t/date (t/instant "1999-12-31T23:59:59"))
115 | ----
116 | ====
117 |
118 | ====
119 | Get the date in another time-zone:
120 | [source.code,clojure]
121 | ----
122 | (t/date (t/in (t/instant "1999-12-31T23:59:59") "Australia/Darwin"))
123 | ----
124 | ====
125 |
126 | ====
127 | Get a specific part of the date:
128 | [source.code,clojure]
129 | ----
130 | (t/year (t/instant "1999-12-31T23:59:59"))
131 | ----
132 | [source.code,clojure]
133 | ----
134 | (t/month (t/instant "1999-12-31T23:59:59"))
135 | ----
136 | [source.code,clojure]
137 | ----
138 | (t/day-of-month (t/instant "1999-12-31T23:59:59"))
139 | ----
140 | ====
141 |
142 |
143 | === Build up times and dates
144 | A unique feature of tick is that you can treat individual units of time
145 | as modular, making it easy to build up and break down time into components.
146 |
147 | ====
148 | Break up an instant:
149 |
150 | ----
151 | (defn instant-breakdown
152 | "Takes an instant of time and breaks it down into units."
153 | [t]
154 | {:day (t/day-of-week t)
155 | :month (t/month t)
156 | :dd (t/day-of-month t)
157 | :MM (t/int (t/month t))
158 | :yyyy (t/int (t/year t))
159 | :mm (t/minute t)
160 | :HH (t/hour t)
161 | :ss (t/second t)})
162 | ----
163 |
164 | ====
165 |
166 | We can treat the individual units of time as building blocks:
167 |
168 | .Tick Time Blocks
169 | [options="header",valign="center"]
170 | |====
171 | 5+|Time 3+|Date |Zone
172 |
173 | 5+|(t/time) 3+|(t/date) |(t/zone)
174 |
175 | |(t/hour)|(t/minute) 3+|(t/second)|(t/year)|(t/month)|(t/day-of-month)|-
176 |
177 | |- |-|(t/millisecond)|(t/microsecond)|(t/nanosecond)|- |- |- |-
178 | |====
179 |
180 | ====
181 | Make up a `time`
182 |
183 | If we want it to be half-past the current hour:
184 | [source.code,clojure]
185 | ----
186 | (t/new-time (t/hour (t/instant)) 30)
187 | ----
188 | Or about lunch time:
189 | [source.code,clojure]
190 | ----
191 | (t/new-time 13 (t/minute (t/instant)))
192 | ----
193 | ====
194 |
195 | ====
196 | Make up a `date-time`
197 | [source.code,clojure]
198 | ----
199 | (t/at (t/date "2018-01-01") (t/time "13:00"))
200 | ----
201 | [source.code,clojure]
202 | ----
203 | (t/on (t/time "13:00") (t/date "2018-01-01"))
204 | ----
205 | [source.code,clojure]
206 | ----
207 | (-> (t/parse "1pm")
208 | (t/on "2018-10-20"))
209 | ----
210 | [source.code,clojure]
211 | ----
212 | (-> (t/tomorrow)
213 | (t/at (t/midnight)))
214 | ----
215 | [source.code,clojure]
216 | ----
217 | (-> (t/noon)
218 | (t/on (t/yesterday)))
219 | ----
220 | ====
221 |
222 | ====
223 | Make up a `Zoned-Date-Time`
224 | [source.code,clojure]
225 | ----
226 | (-> (t/tomorrow)
227 | (t/at (t/midnight))
228 | (t/in "Europe/Paris"))
229 | ----
230 | [source.code,clojure]
231 | ----
232 | (-> (t/tomorrow)
233 | (t/at (t/midnight))
234 | (t/in (t/zone)))
235 | ----
236 | ====
237 |
238 |
239 | === Time and Date manipulation
240 | ====
241 | Give a date a set time in the future:
242 |
243 | [source.code,clojure]
244 | ----
245 | (t/+ (t/date "2000-01-01") (t/new-period 1 :months))
246 | ----
247 |
248 | [source.code,clojure]
249 | ----
250 | (t/+ (t/date "2000-01-01") (t/new-period 4 :weeks))
251 | ----
252 |
253 | [source.code,clojure]
254 | ----
255 | (t/+ (t/date "2000-01-01") (t/new-period 30 :days))
256 | ----
257 |
258 | [source.code,clojure]
259 | ----
260 | (t/+ (t/date "2000-01-01") (t/+ (t/new-period 5 :days)
261 | (t/new-period 1 :weeks)
262 | (t/new-period 10 :months)))
263 | ----
264 |
265 | Or past:
266 |
267 | [source.code,clojure]
268 | ----
269 | (t/- (t/date "2000-01-01") (t/new-period 1 :years))
270 | ----
271 | ====
272 |
273 | ====
274 | Move around in time:
275 | [source.code,clojure]
276 | ----
277 | (t/+ (t/time "12:00") (t/new-duration 5 :minutes))
278 | ----
279 |
280 | [source.code,clojure]
281 | ----
282 | (t/- (t/time "12:00") (t/new-duration 5 :hours))
283 | ----
284 |
285 | [source.code,clojure]
286 | ----
287 | (t/>> (t/time "12:00") (t/+ (t/new-duration 5 :seconds)
288 | (t/new-duration 5 :millis)
289 | (t/new-duration 5 :micros)
290 | (t/new-duration 5 :nanos)))
291 | ----
292 |
293 | Increasing a time by a duration of day magnitude will leave the time
294 | alone - `12:00` in 5 days is still `12:00` (ignoring daylight savings)
295 |
296 | [source.code,clojure]
297 | ----
298 | (t/+ (t/time "12:00") (t/new-duration 5 :days))
299 | ----
300 | ====
301 |
302 | ====
303 | Truncate time to a desired precision:
304 |
305 | [source.code,clojure]
306 | ----
307 | (t/truncate (t/time "10:30:59.99") :minutes)
308 | ----
309 | ====
310 |
311 | ====
312 | Give the am pm time:
313 | ----
314 | (defn twelve-hour-time
315 | "Takes a time and gives the 12 hour display"
316 | [t]
317 | (let [minute (t/minute t)
318 | hour (t/hour t)]
319 | (cond
320 | (= (t/noon) t)
321 | "12:00 NOON"
322 |
323 | (>= hour 13)
324 | (format "%02d:%02d PM" (- hour 12) minute)
325 |
326 | (>= hour 12)
327 | (format "%02d:%02d PM" hour minute)
328 |
329 | (< hour 12)
330 | (format "%02d:%02d AM" hour minute))))
331 |
332 | ----
333 | NOTE: "12 noon is by definition neither *ante meridiem* (before noon) nor *post
334 | meridiem* (after noon), then 12 a.m. refers to midnight at the start of the
335 | specified day (00:00) and 12 p.m. to midnight at the end of that day (24:00)"
336 | - http://www.npl.co.uk/reference/faqs/is-midnight-12-am-or-12-pm-faq-time[NPL]
337 | ====
338 |
--------------------------------------------------------------------------------
/resources/ics/gov.uk/england-and-wales.ics:
--------------------------------------------------------------------------------
1 | BEGIN:VCALENDAR
2 | VERSION:2.0
3 | METHOD:PUBLISH
4 | PRODID:-//uk.gov/GOVUK calendars//EN
5 | CALSCALE:GREGORIAN
6 | BEGIN:VEVENT
7 | DTEND;VALUE=DATE:20150102
8 | DTSTART;VALUE=DATE:20150101
9 | SUMMARY:New Year’s Day
10 | UID:ca6af7456b0088abad9a69f9f620f5ac-0@gov.uk
11 | SEQUENCE:0
12 | DTSTAMP:20191231T121333Z
13 | END:VEVENT
14 | BEGIN:VEVENT
15 | DTEND;VALUE=DATE:20150404
16 | DTSTART;VALUE=DATE:20150403
17 | SUMMARY:Good Friday
18 | UID:ca6af7456b0088abad9a69f9f620f5ac-1@gov.uk
19 | SEQUENCE:0
20 | DTSTAMP:20191231T121333Z
21 | END:VEVENT
22 | BEGIN:VEVENT
23 | DTEND;VALUE=DATE:20150407
24 | DTSTART;VALUE=DATE:20150406
25 | SUMMARY:Easter Monday
26 | UID:ca6af7456b0088abad9a69f9f620f5ac-2@gov.uk
27 | SEQUENCE:0
28 | DTSTAMP:20191231T121333Z
29 | END:VEVENT
30 | BEGIN:VEVENT
31 | DTEND;VALUE=DATE:20150505
32 | DTSTART;VALUE=DATE:20150504
33 | SUMMARY:Early May bank holiday
34 | UID:ca6af7456b0088abad9a69f9f620f5ac-3@gov.uk
35 | SEQUENCE:0
36 | DTSTAMP:20191231T121333Z
37 | END:VEVENT
38 | BEGIN:VEVENT
39 | DTEND;VALUE=DATE:20150526
40 | DTSTART;VALUE=DATE:20150525
41 | SUMMARY:Spring bank holiday
42 | UID:ca6af7456b0088abad9a69f9f620f5ac-4@gov.uk
43 | SEQUENCE:0
44 | DTSTAMP:20191231T121333Z
45 | END:VEVENT
46 | BEGIN:VEVENT
47 | DTEND;VALUE=DATE:20150901
48 | DTSTART;VALUE=DATE:20150831
49 | SUMMARY:Summer bank holiday
50 | UID:ca6af7456b0088abad9a69f9f620f5ac-5@gov.uk
51 | SEQUENCE:0
52 | DTSTAMP:20191231T121333Z
53 | END:VEVENT
54 | BEGIN:VEVENT
55 | DTEND;VALUE=DATE:20151226
56 | DTSTART;VALUE=DATE:20151225
57 | SUMMARY:Christmas Day
58 | UID:ca6af7456b0088abad9a69f9f620f5ac-6@gov.uk
59 | SEQUENCE:0
60 | DTSTAMP:20191231T121333Z
61 | END:VEVENT
62 | BEGIN:VEVENT
63 | DTEND;VALUE=DATE:20151229
64 | DTSTART;VALUE=DATE:20151228
65 | SUMMARY:Boxing Day
66 | UID:ca6af7456b0088abad9a69f9f620f5ac-7@gov.uk
67 | SEQUENCE:0
68 | DTSTAMP:20191231T121333Z
69 | END:VEVENT
70 | BEGIN:VEVENT
71 | DTEND;VALUE=DATE:20160102
72 | DTSTART;VALUE=DATE:20160101
73 | SUMMARY:New Year’s Day
74 | UID:ca6af7456b0088abad9a69f9f620f5ac-8@gov.uk
75 | SEQUENCE:0
76 | DTSTAMP:20191231T121333Z
77 | END:VEVENT
78 | BEGIN:VEVENT
79 | DTEND;VALUE=DATE:20160326
80 | DTSTART;VALUE=DATE:20160325
81 | SUMMARY:Good Friday
82 | UID:ca6af7456b0088abad9a69f9f620f5ac-9@gov.uk
83 | SEQUENCE:0
84 | DTSTAMP:20191231T121333Z
85 | END:VEVENT
86 | BEGIN:VEVENT
87 | DTEND;VALUE=DATE:20160329
88 | DTSTART;VALUE=DATE:20160328
89 | SUMMARY:Easter Monday
90 | UID:ca6af7456b0088abad9a69f9f620f5ac-10@gov.uk
91 | SEQUENCE:0
92 | DTSTAMP:20191231T121333Z
93 | END:VEVENT
94 | BEGIN:VEVENT
95 | DTEND;VALUE=DATE:20160503
96 | DTSTART;VALUE=DATE:20160502
97 | SUMMARY:Early May bank holiday
98 | UID:ca6af7456b0088abad9a69f9f620f5ac-11@gov.uk
99 | SEQUENCE:0
100 | DTSTAMP:20191231T121333Z
101 | END:VEVENT
102 | BEGIN:VEVENT
103 | DTEND;VALUE=DATE:20160531
104 | DTSTART;VALUE=DATE:20160530
105 | SUMMARY:Spring bank holiday
106 | UID:ca6af7456b0088abad9a69f9f620f5ac-12@gov.uk
107 | SEQUENCE:0
108 | DTSTAMP:20191231T121333Z
109 | END:VEVENT
110 | BEGIN:VEVENT
111 | DTEND;VALUE=DATE:20160830
112 | DTSTART;VALUE=DATE:20160829
113 | SUMMARY:Summer bank holiday
114 | UID:ca6af7456b0088abad9a69f9f620f5ac-13@gov.uk
115 | SEQUENCE:0
116 | DTSTAMP:20191231T121333Z
117 | END:VEVENT
118 | BEGIN:VEVENT
119 | DTEND;VALUE=DATE:20161227
120 | DTSTART;VALUE=DATE:20161226
121 | SUMMARY:Boxing Day
122 | UID:ca6af7456b0088abad9a69f9f620f5ac-14@gov.uk
123 | SEQUENCE:0
124 | DTSTAMP:20191231T121333Z
125 | END:VEVENT
126 | BEGIN:VEVENT
127 | DTEND;VALUE=DATE:20161228
128 | DTSTART;VALUE=DATE:20161227
129 | SUMMARY:Christmas Day
130 | UID:ca6af7456b0088abad9a69f9f620f5ac-15@gov.uk
131 | SEQUENCE:0
132 | DTSTAMP:20191231T121333Z
133 | END:VEVENT
134 | BEGIN:VEVENT
135 | DTEND;VALUE=DATE:20170103
136 | DTSTART;VALUE=DATE:20170102
137 | SUMMARY:New Year’s Day
138 | UID:ca6af7456b0088abad9a69f9f620f5ac-16@gov.uk
139 | SEQUENCE:0
140 | DTSTAMP:20191231T121333Z
141 | END:VEVENT
142 | BEGIN:VEVENT
143 | DTEND;VALUE=DATE:20170415
144 | DTSTART;VALUE=DATE:20170414
145 | SUMMARY:Good Friday
146 | UID:ca6af7456b0088abad9a69f9f620f5ac-17@gov.uk
147 | SEQUENCE:0
148 | DTSTAMP:20191231T121333Z
149 | END:VEVENT
150 | BEGIN:VEVENT
151 | DTEND;VALUE=DATE:20170418
152 | DTSTART;VALUE=DATE:20170417
153 | SUMMARY:Easter Monday
154 | UID:ca6af7456b0088abad9a69f9f620f5ac-18@gov.uk
155 | SEQUENCE:0
156 | DTSTAMP:20191231T121333Z
157 | END:VEVENT
158 | BEGIN:VEVENT
159 | DTEND;VALUE=DATE:20170502
160 | DTSTART;VALUE=DATE:20170501
161 | SUMMARY:Early May bank holiday
162 | UID:ca6af7456b0088abad9a69f9f620f5ac-19@gov.uk
163 | SEQUENCE:0
164 | DTSTAMP:20191231T121333Z
165 | END:VEVENT
166 | BEGIN:VEVENT
167 | DTEND;VALUE=DATE:20170530
168 | DTSTART;VALUE=DATE:20170529
169 | SUMMARY:Spring bank holiday
170 | UID:ca6af7456b0088abad9a69f9f620f5ac-20@gov.uk
171 | SEQUENCE:0
172 | DTSTAMP:20191231T121333Z
173 | END:VEVENT
174 | BEGIN:VEVENT
175 | DTEND;VALUE=DATE:20170829
176 | DTSTART;VALUE=DATE:20170828
177 | SUMMARY:Summer bank holiday
178 | UID:ca6af7456b0088abad9a69f9f620f5ac-21@gov.uk
179 | SEQUENCE:0
180 | DTSTAMP:20191231T121333Z
181 | END:VEVENT
182 | BEGIN:VEVENT
183 | DTEND;VALUE=DATE:20171226
184 | DTSTART;VALUE=DATE:20171225
185 | SUMMARY:Christmas Day
186 | UID:ca6af7456b0088abad9a69f9f620f5ac-22@gov.uk
187 | SEQUENCE:0
188 | DTSTAMP:20191231T121333Z
189 | END:VEVENT
190 | BEGIN:VEVENT
191 | DTEND;VALUE=DATE:20171227
192 | DTSTART;VALUE=DATE:20171226
193 | SUMMARY:Boxing Day
194 | UID:ca6af7456b0088abad9a69f9f620f5ac-23@gov.uk
195 | SEQUENCE:0
196 | DTSTAMP:20191231T121333Z
197 | END:VEVENT
198 | BEGIN:VEVENT
199 | DTEND;VALUE=DATE:20180102
200 | DTSTART;VALUE=DATE:20180101
201 | SUMMARY:New Year’s Day
202 | UID:ca6af7456b0088abad9a69f9f620f5ac-24@gov.uk
203 | SEQUENCE:0
204 | DTSTAMP:20191231T121333Z
205 | END:VEVENT
206 | BEGIN:VEVENT
207 | DTEND;VALUE=DATE:20180331
208 | DTSTART;VALUE=DATE:20180330
209 | SUMMARY:Good Friday
210 | UID:ca6af7456b0088abad9a69f9f620f5ac-25@gov.uk
211 | SEQUENCE:0
212 | DTSTAMP:20191231T121333Z
213 | END:VEVENT
214 | BEGIN:VEVENT
215 | DTEND;VALUE=DATE:20180403
216 | DTSTART;VALUE=DATE:20180402
217 | SUMMARY:Easter Monday
218 | UID:ca6af7456b0088abad9a69f9f620f5ac-26@gov.uk
219 | SEQUENCE:0
220 | DTSTAMP:20191231T121333Z
221 | END:VEVENT
222 | BEGIN:VEVENT
223 | DTEND;VALUE=DATE:20180508
224 | DTSTART;VALUE=DATE:20180507
225 | SUMMARY:Early May bank holiday
226 | UID:ca6af7456b0088abad9a69f9f620f5ac-27@gov.uk
227 | SEQUENCE:0
228 | DTSTAMP:20191231T121333Z
229 | END:VEVENT
230 | BEGIN:VEVENT
231 | DTEND;VALUE=DATE:20180529
232 | DTSTART;VALUE=DATE:20180528
233 | SUMMARY:Spring bank holiday
234 | UID:ca6af7456b0088abad9a69f9f620f5ac-28@gov.uk
235 | SEQUENCE:0
236 | DTSTAMP:20191231T121333Z
237 | END:VEVENT
238 | BEGIN:VEVENT
239 | DTEND;VALUE=DATE:20180828
240 | DTSTART;VALUE=DATE:20180827
241 | SUMMARY:Summer bank holiday
242 | UID:ca6af7456b0088abad9a69f9f620f5ac-29@gov.uk
243 | SEQUENCE:0
244 | DTSTAMP:20191231T121333Z
245 | END:VEVENT
246 | BEGIN:VEVENT
247 | DTEND;VALUE=DATE:20181226
248 | DTSTART;VALUE=DATE:20181225
249 | SUMMARY:Christmas Day
250 | UID:ca6af7456b0088abad9a69f9f620f5ac-30@gov.uk
251 | SEQUENCE:0
252 | DTSTAMP:20191231T121333Z
253 | END:VEVENT
254 | BEGIN:VEVENT
255 | DTEND;VALUE=DATE:20181227
256 | DTSTART;VALUE=DATE:20181226
257 | SUMMARY:Boxing Day
258 | UID:ca6af7456b0088abad9a69f9f620f5ac-31@gov.uk
259 | SEQUENCE:0
260 | DTSTAMP:20191231T121333Z
261 | END:VEVENT
262 | BEGIN:VEVENT
263 | DTEND;VALUE=DATE:20190102
264 | DTSTART;VALUE=DATE:20190101
265 | SUMMARY:New Year’s Day
266 | UID:ca6af7456b0088abad9a69f9f620f5ac-32@gov.uk
267 | SEQUENCE:0
268 | DTSTAMP:20191231T121333Z
269 | END:VEVENT
270 | BEGIN:VEVENT
271 | DTEND;VALUE=DATE:20190420
272 | DTSTART;VALUE=DATE:20190419
273 | SUMMARY:Good Friday
274 | UID:ca6af7456b0088abad9a69f9f620f5ac-33@gov.uk
275 | SEQUENCE:0
276 | DTSTAMP:20191231T121333Z
277 | END:VEVENT
278 | BEGIN:VEVENT
279 | DTEND;VALUE=DATE:20190423
280 | DTSTART;VALUE=DATE:20190422
281 | SUMMARY:Easter Monday
282 | UID:ca6af7456b0088abad9a69f9f620f5ac-34@gov.uk
283 | SEQUENCE:0
284 | DTSTAMP:20191231T121333Z
285 | END:VEVENT
286 | BEGIN:VEVENT
287 | DTEND;VALUE=DATE:20190507
288 | DTSTART;VALUE=DATE:20190506
289 | SUMMARY:Early May bank holiday
290 | UID:ca6af7456b0088abad9a69f9f620f5ac-35@gov.uk
291 | SEQUENCE:0
292 | DTSTAMP:20191231T121333Z
293 | END:VEVENT
294 | BEGIN:VEVENT
295 | DTEND;VALUE=DATE:20190528
296 | DTSTART;VALUE=DATE:20190527
297 | SUMMARY:Spring bank holiday
298 | UID:ca6af7456b0088abad9a69f9f620f5ac-36@gov.uk
299 | SEQUENCE:0
300 | DTSTAMP:20191231T121333Z
301 | END:VEVENT
302 | BEGIN:VEVENT
303 | DTEND;VALUE=DATE:20190827
304 | DTSTART;VALUE=DATE:20190826
305 | SUMMARY:Summer bank holiday
306 | UID:ca6af7456b0088abad9a69f9f620f5ac-37@gov.uk
307 | SEQUENCE:0
308 | DTSTAMP:20191231T121333Z
309 | END:VEVENT
310 | BEGIN:VEVENT
311 | DTEND;VALUE=DATE:20191226
312 | DTSTART;VALUE=DATE:20191225
313 | SUMMARY:Christmas Day
314 | UID:ca6af7456b0088abad9a69f9f620f5ac-38@gov.uk
315 | SEQUENCE:0
316 | DTSTAMP:20191231T121333Z
317 | END:VEVENT
318 | BEGIN:VEVENT
319 | DTEND;VALUE=DATE:20191227
320 | DTSTART;VALUE=DATE:20191226
321 | SUMMARY:Boxing Day
322 | UID:ca6af7456b0088abad9a69f9f620f5ac-39@gov.uk
323 | SEQUENCE:0
324 | DTSTAMP:20191231T121333Z
325 | END:VEVENT
326 | BEGIN:VEVENT
327 | DTEND;VALUE=DATE:20200102
328 | DTSTART;VALUE=DATE:20200101
329 | SUMMARY:New Year’s Day
330 | UID:ca6af7456b0088abad9a69f9f620f5ac-40@gov.uk
331 | SEQUENCE:0
332 | DTSTAMP:20191231T121333Z
333 | END:VEVENT
334 | BEGIN:VEVENT
335 | DTEND;VALUE=DATE:20200411
336 | DTSTART;VALUE=DATE:20200410
337 | SUMMARY:Good Friday
338 | UID:ca6af7456b0088abad9a69f9f620f5ac-41@gov.uk
339 | SEQUENCE:0
340 | DTSTAMP:20191231T121333Z
341 | END:VEVENT
342 | BEGIN:VEVENT
343 | DTEND;VALUE=DATE:20200414
344 | DTSTART;VALUE=DATE:20200413
345 | SUMMARY:Easter Monday
346 | UID:ca6af7456b0088abad9a69f9f620f5ac-42@gov.uk
347 | SEQUENCE:0
348 | DTSTAMP:20191231T121333Z
349 | END:VEVENT
350 | BEGIN:VEVENT
351 | DTEND;VALUE=DATE:20200509
352 | DTSTART;VALUE=DATE:20200508
353 | SUMMARY:Early May bank holiday (VE day)
354 | UID:ca6af7456b0088abad9a69f9f620f5ac-43@gov.uk
355 | SEQUENCE:0
356 | DTSTAMP:20191231T121333Z
357 | END:VEVENT
358 | BEGIN:VEVENT
359 | DTEND;VALUE=DATE:20200526
360 | DTSTART;VALUE=DATE:20200525
361 | SUMMARY:Spring bank holiday
362 | UID:ca6af7456b0088abad9a69f9f620f5ac-44@gov.uk
363 | SEQUENCE:0
364 | DTSTAMP:20191231T121333Z
365 | END:VEVENT
366 | BEGIN:VEVENT
367 | DTEND;VALUE=DATE:20200901
368 | DTSTART;VALUE=DATE:20200831
369 | SUMMARY:Summer bank holiday
370 | UID:ca6af7456b0088abad9a69f9f620f5ac-45@gov.uk
371 | SEQUENCE:0
372 | DTSTAMP:20191231T121333Z
373 | END:VEVENT
374 | BEGIN:VEVENT
375 | DTEND;VALUE=DATE:20201226
376 | DTSTART;VALUE=DATE:20201225
377 | SUMMARY:Christmas Day
378 | UID:ca6af7456b0088abad9a69f9f620f5ac-46@gov.uk
379 | SEQUENCE:0
380 | DTSTAMP:20191231T121333Z
381 | END:VEVENT
382 | BEGIN:VEVENT
383 | DTEND;VALUE=DATE:20201229
384 | DTSTART;VALUE=DATE:20201228
385 | SUMMARY:Boxing Day
386 | UID:ca6af7456b0088abad9a69f9f620f5ac-47@gov.uk
387 | SEQUENCE:0
388 | DTSTAMP:20191231T121333Z
389 | END:VEVENT
390 | BEGIN:VEVENT
391 | DTEND;VALUE=DATE:20210102
392 | DTSTART;VALUE=DATE:20210101
393 | SUMMARY:New Year’s Day
394 | UID:ca6af7456b0088abad9a69f9f620f5ac-48@gov.uk
395 | SEQUENCE:0
396 | DTSTAMP:20191231T121333Z
397 | END:VEVENT
398 | BEGIN:VEVENT
399 | DTEND;VALUE=DATE:20210403
400 | DTSTART;VALUE=DATE:20210402
401 | SUMMARY:Good Friday
402 | UID:ca6af7456b0088abad9a69f9f620f5ac-49@gov.uk
403 | SEQUENCE:0
404 | DTSTAMP:20191231T121333Z
405 | END:VEVENT
406 | BEGIN:VEVENT
407 | DTEND;VALUE=DATE:20210406
408 | DTSTART;VALUE=DATE:20210405
409 | SUMMARY:Easter Monday
410 | UID:ca6af7456b0088abad9a69f9f620f5ac-50@gov.uk
411 | SEQUENCE:0
412 | DTSTAMP:20191231T121333Z
413 | END:VEVENT
414 | BEGIN:VEVENT
415 | DTEND;VALUE=DATE:20210504
416 | DTSTART;VALUE=DATE:20210503
417 | SUMMARY:Early May bank holiday
418 | UID:ca6af7456b0088abad9a69f9f620f5ac-51@gov.uk
419 | SEQUENCE:0
420 | DTSTAMP:20191231T121333Z
421 | END:VEVENT
422 | BEGIN:VEVENT
423 | DTEND;VALUE=DATE:20210601
424 | DTSTART;VALUE=DATE:20210531
425 | SUMMARY:Spring bank holiday
426 | UID:ca6af7456b0088abad9a69f9f620f5ac-52@gov.uk
427 | SEQUENCE:0
428 | DTSTAMP:20191231T121333Z
429 | END:VEVENT
430 | BEGIN:VEVENT
431 | DTEND;VALUE=DATE:20210831
432 | DTSTART;VALUE=DATE:20210830
433 | SUMMARY:Summer bank holiday
434 | UID:ca6af7456b0088abad9a69f9f620f5ac-53@gov.uk
435 | SEQUENCE:0
436 | DTSTAMP:20191231T121333Z
437 | END:VEVENT
438 | BEGIN:VEVENT
439 | DTEND;VALUE=DATE:20211228
440 | DTSTART;VALUE=DATE:20211227
441 | SUMMARY:Christmas Day
442 | UID:ca6af7456b0088abad9a69f9f620f5ac-54@gov.uk
443 | SEQUENCE:0
444 | DTSTAMP:20191231T121333Z
445 | END:VEVENT
446 | BEGIN:VEVENT
447 | DTEND;VALUE=DATE:20211229
448 | DTSTART;VALUE=DATE:20211228
449 | SUMMARY:Boxing Day
450 | UID:ca6af7456b0088abad9a69f9f620f5ac-55@gov.uk
451 | SEQUENCE:0
452 | DTSTAMP:20191231T121333Z
453 | END:VEVENT
454 | END:VCALENDAR
455 |
--------------------------------------------------------------------------------
/dev/resources/stopwatch-25763.svg:
--------------------------------------------------------------------------------
1 |
2 |
118 |
--------------------------------------------------------------------------------
/src/tick/alpha/api.cljc:
--------------------------------------------------------------------------------
1 | ;; Copyright © 2016-2017, JUXT LTD.
2 |
3 | (ns tick.alpha.api
4 | (:refer-clojure
5 | :exclude [+ - * inc dec max min conj
6 | range time int long complement
7 | < <= > >= << >>
8 | extend
9 | atom swap! swap-vals! compare-and-set!
10 | reset! reset-vals!
11 | second
12 | group-by divide format] )
13 | (:require
14 | [clojure.spec.alpha :as s]
15 | [tick.core :as core]
16 | [tick.format :as t.f]
17 | #?(:clj tick.file) ; To ensure protocol extension
18 | #?(:clj [net.cgrand.macrovich :as macros])
19 | [tick.interval :as interval]
20 | [clojure.set :as set]
21 | #?@(:cljs
22 | [
23 | [java.time :refer [Duration ZoneId LocalTime LocalDate DayOfWeek Month ZoneOffset]]
24 | [java.time.format :refer [DateTimeFormatter]]]))
25 | #?(:cljs
26 | (:require-macros
27 | [net.cgrand.macrovich :as macros]
28 | [tick.alpha.api :refer [with-clock]]))
29 | #?(:clj
30 | (:import
31 | [java.time Duration ZoneId LocalTime LocalDate DayOfWeek Month ZoneOffset]
32 | [java.time.format DateTimeFormatter])))
33 |
34 | ;; This API is optimises convenience, API stability and (type) safety
35 | ;; over performance. Where performance is critical, use tick.core and
36 | ;; friends.
37 |
38 | ;; clojure.spec assertions are used to check correctness, but these
39 | ;; are disabled by default (except when testing).
40 |
41 | ;; Construction
42 |
43 | (def new-time core/new-time)
44 | (def new-date core/new-date)
45 |
46 | ;; Surfacing some useful constants
47 |
48 | (def unit-map core/unit-map)
49 |
50 | ;; Point-in-time 'demo' functions
51 |
52 | (defn now [] (core/now))
53 | (defn today [] (core/today))
54 | (defn tomorrow [] (core/tomorrow))
55 | (defn yesterday [] (core/yesterday))
56 |
57 | ;; Conversions, with 0-arity defaults
58 |
59 | (defn time
60 | ([] (core/time (now)))
61 | ([v] (core/time v)))
62 |
63 | (defn date
64 | ([] (today))
65 | ([v] (core/date v)))
66 |
67 | (defn inst
68 | ([] (core/inst (now)))
69 | ([v] (core/inst v)))
70 |
71 | (defn instant
72 | ([] (core/instant (now)))
73 | ([v] (core/instant v)))
74 |
75 | (defn date-time
76 | ([] (core/date-time (now)))
77 | ([v] (core/date-time v)))
78 |
79 | (defn offset-date-time
80 | ([] (core/offset-date-time (now)))
81 | ([v] (core/offset-date-time v)))
82 |
83 | (defn zoned-date-time
84 | ([] (core/zoned-date-time (now)))
85 | ([v] (core/zoned-date-time v)))
86 |
87 | ;; Extraction
88 |
89 | (defn nanosecond [t] (core/nanosecond t))
90 | (defn microsecond [t] (core/microsecond t))
91 | (defn millisecond [t] (core/millisecond t))
92 | (defn second [t] (core/second t))
93 | (defn minute [t] (core/minute t))
94 | (defn hour [t] (core/hour t))
95 |
96 | (defn day-of-week
97 | ([] (core/day-of-week (today)))
98 | ([v] (core/day-of-week v)))
99 |
100 | (defn day-of-month
101 | ([] (core/day-of-month (today)))
102 | ([v] (core/day-of-month v)))
103 |
104 | (defn month
105 | ([] (core/month (today)))
106 | ([v] (core/month v)))
107 |
108 | (defn year
109 | ([] (core/year (today)))
110 | ([v] (core/year v)))
111 |
112 | (defn year-month
113 | ([] (core/year-month (today)))
114 | ([v] (core/year-month v)))
115 |
116 | (defn zone
117 | ([] (core/current-zone))
118 | ([z] (core/zone z)))
119 |
120 | (defn zone-offset
121 | ([offset] (core/zone-offset offset))
122 | ([hours minutes] (. ZoneOffset ofHoursMinutes hours minutes))
123 | ([hours minutes seconds] (. ZoneOffset ofHoursMinutesSeconds hours minutes seconds)))
124 |
125 | ;; Reification
126 |
127 | (defn on [t d] (core/on t (date d)))
128 | (defn at [d t] (core/at d (time t)))
129 | (defn in [ldt z] (core/in ldt (zone z)))
130 | (defn offset-by [ldt offset] (core/offset-by ldt (zone-offset offset)))
131 |
132 | ;; Constants
133 |
134 | (def MONDAY (. DayOfWeek -MONDAY))
135 | (def TUESDAY (. DayOfWeek -TUESDAY))
136 | (def WEDNESDAY (. DayOfWeek -WEDNESDAY))
137 | (def THURSDAY (. DayOfWeek -THURSDAY))
138 | (def FRIDAY (. DayOfWeek -FRIDAY))
139 | (def SATURDAY (. DayOfWeek -SATURDAY))
140 | (def SUNDAY (. DayOfWeek -SUNDAY))
141 |
142 | (def JANUARY (. Month -JANUARY))
143 | (def FEBRUARY (. Month -FEBRUARY))
144 | (def MARCH (. Month -MARCH))
145 | (def APRIL (. Month -APRIL))
146 | (def MAY (. Month -MAY))
147 | (def JUNE (. Month -JUNE))
148 | (def JULY (. Month -JULY))
149 | (def AUGUST (. Month -AUGUST))
150 | (def SEPTEMBER (. Month -SEPTEMBER))
151 | (def OCTOBER (. Month -OCTOBER))
152 | (def NOVEMBER (. Month -NOVEMBER))
153 | (def DECEMBER (. Month -DECEMBER))
154 |
155 | (defn beginning [v] (core/beginning v))
156 | (defn end [v] (core/end v))
157 | (defn duration [v] (core/duration v))
158 |
159 | (def coincident? core/coincident?)
160 |
161 | (def noon core/noon)
162 | (def midnight core/midnight)
163 | (def midnight? core/midnight?)
164 | (def epoch core/epoch)
165 |
166 | (def fields core/fields)
167 | (def with core/with)
168 | (def ago core/ago)
169 | (def hence core/hence)
170 |
171 | ;; Zones
172 |
173 | (def UTC (zone "UTC"))
174 | ;(def LONDON (zone "Europe/London"))
175 |
176 | ;; Parsing
177 |
178 | (def parse core/parse)
179 |
180 | ;; Arithmetic
181 |
182 | (defn +
183 | ([] (. Duration -ZERO))
184 | ([arg] arg)
185 | ([arg & args]
186 | (reduce #(core/+ %1 %2) arg args)))
187 |
188 | (defn -
189 | ([] (. Duration -ZERO))
190 | ([arg] (core/negated arg))
191 | ([arg & args]
192 | (reduce #(core/- %1 %2) arg args)))
193 |
194 | (defn inc [t]
195 | (core/inc t))
196 |
197 | (defn dec [t]
198 | (core/dec t))
199 |
200 | (defn >> [t amt]
201 | (core/>> t amt))
202 |
203 | (defn << [t amt]
204 | (core/<< t amt))
205 |
206 | (def max core/max)
207 | (def min core/min)
208 |
209 | (def min-of-type core/min-of-type)
210 | (def max-of-type core/max-of-type)
211 |
212 | (def range core/range)
213 |
214 | (defn int [arg] (core/int arg))
215 | (defn long [arg] (core/long arg))
216 |
217 | ;; Lengths of time (durations & periods)
218 |
219 | (defn nanos [v] (core/nanos v))
220 | (defn micros [v] (core/micros v))
221 | (defn millis [v] (core/millis v))
222 | (defn seconds [v] (core/seconds v))
223 | (defn minutes [v] (core/minutes v))
224 | (defn hours [v] (core/hours v))
225 | (defn days [v] (core/days v))
226 | (defn months [v] (core/months v))
227 | (defn years [v] (core/years v))
228 |
229 | ;; Units
230 | (def units core/units)
231 |
232 | ;; Truncation
233 | (def truncate core/truncate)
234 |
235 | ;; Comparisons
236 |
237 | (defn <
238 | ([x] true)
239 | ([x y] (core/< x y))
240 | ([x y & more] (if (core/< x y)
241 | (if (next more)
242 | (recur y (first more) (next more))
243 | (core/< y (first more)))
244 | false)))
245 |
246 | (defn <=
247 | ([x] true)
248 | ([x y] (core/<= x y))
249 | ([x y & more] (if (core/<= x y)
250 | (if (next more)
251 | (recur y (first more) (next more))
252 | (core/<= y (first more)))
253 | false)))
254 |
255 | (defn >
256 | ([x] true)
257 | ([x y] (core/> x y))
258 | ([x y & more] (if (core/> x y)
259 | (if (next more)
260 | (recur y (first more) (next more))
261 | (core/> y (first more)))
262 | false)))
263 |
264 | (defn >=
265 | ([x] true)
266 | ([x y] (core/>= x y))
267 | ([x y & more] (if (core/>= x y)
268 | (if (next more)
269 | (recur y (first more) (next more))
270 | (core/>= y (first more)))
271 | false)))
272 |
273 | ;; TODO: Multiplication (of durations)
274 |
275 | ;; Clocks
276 |
277 | ;; Fixing the clock used for `today` and `now`.
278 |
279 | (defn clock
280 | ([] (core/current-clock))
281 | ([i] (core/clock i)))
282 |
283 | (defmacro with-clock [^java.time.Clock clock & body]
284 | `(binding [tick.core/*clock* (core/clock ~clock)]
285 | ~@body))
286 |
287 | ;(def tick core/tick)
288 | (def atom core/atom)
289 | (def swap! core/swap!)
290 | (def swap-vals! core/swap-vals!)
291 | (def compare-and-set! core/compare-and-set!)
292 | (def reset! core/reset!)
293 | (def reset-vals! core/reset-vals!)
294 |
295 | ;; Intervals
296 |
297 | (defn new-interval [x y]
298 | (interval/new-interval x y))
299 |
300 | (defn extend [ival & durations]
301 | (reduce interval/extend ival durations))
302 |
303 | (defn scale [ival & durations]
304 | (reduce interval/extend ival durations))
305 |
306 | (def ^{:doc "Return an interval which forms the bounding-box of the given arguments."}
307 | bounds interval/bounds)
308 |
309 | (defn am [^LocalDate date] (interval/am date))
310 | (defn pm [^LocalDate date] (interval/pm date))
311 |
312 | (defn relation [i1 i2]
313 | (interval/relation i1 i2))
314 |
315 | (defn new-duration
316 | [n u]
317 | (core/new-duration n u))
318 |
319 | (defn new-period
320 | [n u]
321 | (core/new-period n u))
322 |
323 | (defn between [v1 v2] (core/between v1 v2))
324 |
325 | (defn concur
326 | ([] nil)
327 | ([x] x)
328 | ([x & args]
329 | (reduce interval/concur x args)))
330 |
331 | (defn concurrencies [& intervals]
332 | (apply interval/concurrencies intervals))
333 |
334 | ;; Divisions
335 |
336 | (defn divide-by [divisor t]
337 | (core/divide t divisor))
338 |
339 | ;; Alternative useful for -> threading
340 | (defn divide [t divisor]
341 | (core/divide t divisor))
342 |
343 | ;; Temporal adjusters
344 |
345 | #_(defn adjust [t adjuster]
346 | (core/adjust t adjuster))
347 |
348 | ;; Useful functions
349 |
350 | #_(defn dates-over [interval]
351 | (let [interval (interval/interval interval)]
352 | (s/assert :tick.interval/interval interval)
353 | (interval/dates-over interval)))
354 |
355 | #_(defn year-months-over [interval]
356 | (let [interval (interval/interval interval)]
357 | (s/assert :tick.interval/interval interval)
358 | (interval/year-months-over interval)))
359 |
360 | #_(defn years-over [interval]
361 | (let [interval (interval/interval interval)]
362 | (s/assert :tick.interval/interval interval)
363 | (interval/years-over interval)))
364 |
365 | ;; Note: Not sure about partition here for an individual interval. Should reserve for interval sets.
366 |
367 | #_(defn segment-by [f interval]
368 | (let [interval (interval/interval interval)]
369 | (s/assert :tick.interval/interval interval)
370 | (interval/segment-by f interval)))
371 |
372 | #_(defn segment-by-date [interval]
373 | (segment-by interval/dates-over interval))
374 |
375 | #_(defn group-segments-by [f interval]
376 | (let [interval (interval/interval interval)]
377 | (s/assert :tick.interval/interval interval)
378 | (interval/group-segments-by f interval)))
379 |
380 | #_(defn group-segments-by-date [interval]
381 | (group-segments-by interval/dates-over interval))
382 |
383 | ;; Interval sets
384 |
385 | (def ordered-disjoint-intervals? interval/ordered-disjoint-intervals?)
386 | (def unite interval/unite)
387 | (def normalize interval/normalize)
388 | (def union interval/union)
389 | (def conj interval/conj)
390 | (def intersection interval/intersection)
391 | (def intersects? interval/intersects?)
392 | (def difference interval/difference)
393 | (def complement interval/complement)
394 | (def group-by interval/group-by)
395 |
396 | ;; Formatting
397 | (defn format
398 | ([o] (t.f/format o))
399 | ([fmt o]
400 | (t.f/format fmt o)))
401 |
402 | (defn ^DateTimeFormatter formatter
403 | "Constructs a DateTimeFormatter out of either a
404 |
405 | * format string - \"YYYY/mm/DD\" \"YYY HH:MM\" etc.
406 | or
407 | * formatter name - :iso-instant :iso-date etc"
408 | ([fmt]
409 | (t.f/formatter fmt))
410 | ([fmt locale]
411 | (t.f/formatter fmt locale)))
412 |
413 | (defn clock?
414 | "Return whether the provided value `v` is a clock"
415 | [v] (core/clock? v))
416 | (defn day-of-week?
417 | "Return whether the provided value `v` is a day of the week"
418 | [v] (core/day-of-week? v))
419 | (defn duration?
420 | "Return whether the provided value `v` is a duration"
421 | [v] (core/duration? v))
422 | (defn instant?
423 | "Return whether the provided value `v` is an instant"
424 | [v] (core/instant? v))
425 | (defn date?
426 | "Return whether the provided value `v` is a date"
427 | [v] (core/date? v))
428 | (defn date-time?
429 | "Return whether the provided value `v` is a date time"
430 | [v] (core/date-time? v))
431 | (defn time?
432 | "Return whether the provided value `v` is a time"
433 | [v] (core/time? v))
434 | (defn month?
435 | "Return whether the provided value `v` is a month"
436 | [v] (core/month? v))
437 | (defn offset-date-time?
438 | "Return whether the provided value `v` is an offset date time"
439 | [v] (core/offset-date-time? v))
440 | (defn period?
441 | "Return whether the provided value `v` is a period"
442 | [v] (core/period? v))
443 | (defn year?
444 | "Return whether the provided value `v` is a year"
445 | [v] (core/year? v))
446 | (defn year-month?
447 | "Return whether the provided value `v` is a year month"
448 | [v] (core/year-month? v))
449 | (defn zone?
450 | "Return whether the provided value `v` is a zone time zone"
451 | [v] (core/zone? v))
452 | (defn zone-offset?
453 | "Return whether the provided value `v` is a zone offset"
454 | [v] (core/zone-offset? v))
455 | (defn zoned-date-time?
456 | "Return whether the provided value `v` is a zoned date time"
457 | [v] (core/zoned-date-time? v))
458 | (defn interval?
459 | "Return whether the provided value `v` is an interval"
460 | [v] (core/interval? v))
461 |
--------------------------------------------------------------------------------
/dev/resources/calendar-152134.svg:
--------------------------------------------------------------------------------
1 |
2 |
109 |
--------------------------------------------------------------------------------
/resources/ics/gov.uk/scotland.ics:
--------------------------------------------------------------------------------
1 | BEGIN:VCALENDAR
2 | VERSION:2.0
3 | METHOD:PUBLISH
4 | PRODID:-//uk.gov/GOVUK calendars//EN
5 | CALSCALE:GREGORIAN
6 | BEGIN:VEVENT
7 | DTEND;VALUE=DATE:20150102
8 | DTSTART;VALUE=DATE:20150101
9 | SUMMARY:New Year’s Day
10 | UID:8a443d7e3222342742d91fa8535df82a-0@gov.uk
11 | SEQUENCE:0
12 | DTSTAMP:20191231T121333Z
13 | END:VEVENT
14 | BEGIN:VEVENT
15 | DTEND;VALUE=DATE:20150103
16 | DTSTART;VALUE=DATE:20150102
17 | SUMMARY:2nd January
18 | UID:8a443d7e3222342742d91fa8535df82a-1@gov.uk
19 | SEQUENCE:0
20 | DTSTAMP:20191231T121333Z
21 | END:VEVENT
22 | BEGIN:VEVENT
23 | DTEND;VALUE=DATE:20150404
24 | DTSTART;VALUE=DATE:20150403
25 | SUMMARY:Good Friday
26 | UID:8a443d7e3222342742d91fa8535df82a-2@gov.uk
27 | SEQUENCE:0
28 | DTSTAMP:20191231T121333Z
29 | END:VEVENT
30 | BEGIN:VEVENT
31 | DTEND;VALUE=DATE:20150505
32 | DTSTART;VALUE=DATE:20150504
33 | SUMMARY:Early May bank holiday
34 | UID:8a443d7e3222342742d91fa8535df82a-3@gov.uk
35 | SEQUENCE:0
36 | DTSTAMP:20191231T121333Z
37 | END:VEVENT
38 | BEGIN:VEVENT
39 | DTEND;VALUE=DATE:20150526
40 | DTSTART;VALUE=DATE:20150525
41 | SUMMARY:Spring bank holiday
42 | UID:8a443d7e3222342742d91fa8535df82a-4@gov.uk
43 | SEQUENCE:0
44 | DTSTAMP:20191231T121333Z
45 | END:VEVENT
46 | BEGIN:VEVENT
47 | DTEND;VALUE=DATE:20150804
48 | DTSTART;VALUE=DATE:20150803
49 | SUMMARY:Summer bank holiday
50 | UID:8a443d7e3222342742d91fa8535df82a-5@gov.uk
51 | SEQUENCE:0
52 | DTSTAMP:20191231T121333Z
53 | END:VEVENT
54 | BEGIN:VEVENT
55 | DTEND;VALUE=DATE:20151201
56 | DTSTART;VALUE=DATE:20151130
57 | SUMMARY:St Andrew’s Day
58 | UID:8a443d7e3222342742d91fa8535df82a-6@gov.uk
59 | SEQUENCE:0
60 | DTSTAMP:20191231T121333Z
61 | END:VEVENT
62 | BEGIN:VEVENT
63 | DTEND;VALUE=DATE:20151226
64 | DTSTART;VALUE=DATE:20151225
65 | SUMMARY:Christmas Day
66 | UID:8a443d7e3222342742d91fa8535df82a-7@gov.uk
67 | SEQUENCE:0
68 | DTSTAMP:20191231T121333Z
69 | END:VEVENT
70 | BEGIN:VEVENT
71 | DTEND;VALUE=DATE:20151229
72 | DTSTART;VALUE=DATE:20151228
73 | SUMMARY:Boxing Day
74 | UID:8a443d7e3222342742d91fa8535df82a-8@gov.uk
75 | SEQUENCE:0
76 | DTSTAMP:20191231T121333Z
77 | END:VEVENT
78 | BEGIN:VEVENT
79 | DTEND;VALUE=DATE:20160102
80 | DTSTART;VALUE=DATE:20160101
81 | SUMMARY:New Year’s Day
82 | UID:8a443d7e3222342742d91fa8535df82a-9@gov.uk
83 | SEQUENCE:0
84 | DTSTAMP:20191231T121333Z
85 | END:VEVENT
86 | BEGIN:VEVENT
87 | DTEND;VALUE=DATE:20160105
88 | DTSTART;VALUE=DATE:20160104
89 | SUMMARY:2nd January
90 | UID:8a443d7e3222342742d91fa8535df82a-10@gov.uk
91 | SEQUENCE:0
92 | DTSTAMP:20191231T121333Z
93 | END:VEVENT
94 | BEGIN:VEVENT
95 | DTEND;VALUE=DATE:20160326
96 | DTSTART;VALUE=DATE:20160325
97 | SUMMARY:Good Friday
98 | UID:8a443d7e3222342742d91fa8535df82a-11@gov.uk
99 | SEQUENCE:0
100 | DTSTAMP:20191231T121333Z
101 | END:VEVENT
102 | BEGIN:VEVENT
103 | DTEND;VALUE=DATE:20160503
104 | DTSTART;VALUE=DATE:20160502
105 | SUMMARY:Early May bank holiday
106 | UID:8a443d7e3222342742d91fa8535df82a-12@gov.uk
107 | SEQUENCE:0
108 | DTSTAMP:20191231T121333Z
109 | END:VEVENT
110 | BEGIN:VEVENT
111 | DTEND;VALUE=DATE:20160531
112 | DTSTART;VALUE=DATE:20160530
113 | SUMMARY:Spring bank holiday
114 | UID:8a443d7e3222342742d91fa8535df82a-13@gov.uk
115 | SEQUENCE:0
116 | DTSTAMP:20191231T121333Z
117 | END:VEVENT
118 | BEGIN:VEVENT
119 | DTEND;VALUE=DATE:20160802
120 | DTSTART;VALUE=DATE:20160801
121 | SUMMARY:Summer bank holiday
122 | UID:8a443d7e3222342742d91fa8535df82a-14@gov.uk
123 | SEQUENCE:0
124 | DTSTAMP:20191231T121333Z
125 | END:VEVENT
126 | BEGIN:VEVENT
127 | DTEND;VALUE=DATE:20161201
128 | DTSTART;VALUE=DATE:20161130
129 | SUMMARY:St Andrew’s Day
130 | UID:8a443d7e3222342742d91fa8535df82a-15@gov.uk
131 | SEQUENCE:0
132 | DTSTAMP:20191231T121333Z
133 | END:VEVENT
134 | BEGIN:VEVENT
135 | DTEND;VALUE=DATE:20161227
136 | DTSTART;VALUE=DATE:20161226
137 | SUMMARY:Boxing Day
138 | UID:8a443d7e3222342742d91fa8535df82a-16@gov.uk
139 | SEQUENCE:0
140 | DTSTAMP:20191231T121333Z
141 | END:VEVENT
142 | BEGIN:VEVENT
143 | DTEND;VALUE=DATE:20161228
144 | DTSTART;VALUE=DATE:20161227
145 | SUMMARY:Christmas Day
146 | UID:8a443d7e3222342742d91fa8535df82a-17@gov.uk
147 | SEQUENCE:0
148 | DTSTAMP:20191231T121333Z
149 | END:VEVENT
150 | BEGIN:VEVENT
151 | DTEND;VALUE=DATE:20170103
152 | DTSTART;VALUE=DATE:20170102
153 | SUMMARY:2nd January
154 | UID:8a443d7e3222342742d91fa8535df82a-18@gov.uk
155 | SEQUENCE:0
156 | DTSTAMP:20191231T121333Z
157 | END:VEVENT
158 | BEGIN:VEVENT
159 | DTEND;VALUE=DATE:20170104
160 | DTSTART;VALUE=DATE:20170103
161 | SUMMARY:New Year’s Day
162 | UID:8a443d7e3222342742d91fa8535df82a-19@gov.uk
163 | SEQUENCE:0
164 | DTSTAMP:20191231T121333Z
165 | END:VEVENT
166 | BEGIN:VEVENT
167 | DTEND;VALUE=DATE:20170415
168 | DTSTART;VALUE=DATE:20170414
169 | SUMMARY:Good Friday
170 | UID:8a443d7e3222342742d91fa8535df82a-20@gov.uk
171 | SEQUENCE:0
172 | DTSTAMP:20191231T121333Z
173 | END:VEVENT
174 | BEGIN:VEVENT
175 | DTEND;VALUE=DATE:20170502
176 | DTSTART;VALUE=DATE:20170501
177 | SUMMARY:Early May bank holiday
178 | UID:8a443d7e3222342742d91fa8535df82a-21@gov.uk
179 | SEQUENCE:0
180 | DTSTAMP:20191231T121333Z
181 | END:VEVENT
182 | BEGIN:VEVENT
183 | DTEND;VALUE=DATE:20170530
184 | DTSTART;VALUE=DATE:20170529
185 | SUMMARY:Spring bank holiday
186 | UID:8a443d7e3222342742d91fa8535df82a-22@gov.uk
187 | SEQUENCE:0
188 | DTSTAMP:20191231T121333Z
189 | END:VEVENT
190 | BEGIN:VEVENT
191 | DTEND;VALUE=DATE:20170808
192 | DTSTART;VALUE=DATE:20170807
193 | SUMMARY:Summer bank holiday
194 | UID:8a443d7e3222342742d91fa8535df82a-23@gov.uk
195 | SEQUENCE:0
196 | DTSTAMP:20191231T121333Z
197 | END:VEVENT
198 | BEGIN:VEVENT
199 | DTEND;VALUE=DATE:20171201
200 | DTSTART;VALUE=DATE:20171130
201 | SUMMARY:St Andrew’s Day
202 | UID:8a443d7e3222342742d91fa8535df82a-24@gov.uk
203 | SEQUENCE:0
204 | DTSTAMP:20191231T121333Z
205 | END:VEVENT
206 | BEGIN:VEVENT
207 | DTEND;VALUE=DATE:20171226
208 | DTSTART;VALUE=DATE:20171225
209 | SUMMARY:Christmas Day
210 | UID:8a443d7e3222342742d91fa8535df82a-25@gov.uk
211 | SEQUENCE:0
212 | DTSTAMP:20191231T121333Z
213 | END:VEVENT
214 | BEGIN:VEVENT
215 | DTEND;VALUE=DATE:20171227
216 | DTSTART;VALUE=DATE:20171226
217 | SUMMARY:Boxing Day
218 | UID:8a443d7e3222342742d91fa8535df82a-26@gov.uk
219 | SEQUENCE:0
220 | DTSTAMP:20191231T121333Z
221 | END:VEVENT
222 | BEGIN:VEVENT
223 | DTEND;VALUE=DATE:20180102
224 | DTSTART;VALUE=DATE:20180101
225 | SUMMARY:New Year’s Day
226 | UID:8a443d7e3222342742d91fa8535df82a-27@gov.uk
227 | SEQUENCE:0
228 | DTSTAMP:20191231T121333Z
229 | END:VEVENT
230 | BEGIN:VEVENT
231 | DTEND;VALUE=DATE:20180103
232 | DTSTART;VALUE=DATE:20180102
233 | SUMMARY:2nd January
234 | UID:8a443d7e3222342742d91fa8535df82a-28@gov.uk
235 | SEQUENCE:0
236 | DTSTAMP:20191231T121333Z
237 | END:VEVENT
238 | BEGIN:VEVENT
239 | DTEND;VALUE=DATE:20180331
240 | DTSTART;VALUE=DATE:20180330
241 | SUMMARY:Good Friday
242 | UID:8a443d7e3222342742d91fa8535df82a-29@gov.uk
243 | SEQUENCE:0
244 | DTSTAMP:20191231T121333Z
245 | END:VEVENT
246 | BEGIN:VEVENT
247 | DTEND;VALUE=DATE:20180508
248 | DTSTART;VALUE=DATE:20180507
249 | SUMMARY:Early May bank holiday
250 | UID:8a443d7e3222342742d91fa8535df82a-30@gov.uk
251 | SEQUENCE:0
252 | DTSTAMP:20191231T121333Z
253 | END:VEVENT
254 | BEGIN:VEVENT
255 | DTEND;VALUE=DATE:20180529
256 | DTSTART;VALUE=DATE:20180528
257 | SUMMARY:Spring bank holiday
258 | UID:8a443d7e3222342742d91fa8535df82a-31@gov.uk
259 | SEQUENCE:0
260 | DTSTAMP:20191231T121333Z
261 | END:VEVENT
262 | BEGIN:VEVENT
263 | DTEND;VALUE=DATE:20180807
264 | DTSTART;VALUE=DATE:20180806
265 | SUMMARY:Summer bank holiday
266 | UID:8a443d7e3222342742d91fa8535df82a-32@gov.uk
267 | SEQUENCE:0
268 | DTSTAMP:20191231T121333Z
269 | END:VEVENT
270 | BEGIN:VEVENT
271 | DTEND;VALUE=DATE:20181201
272 | DTSTART;VALUE=DATE:20181130
273 | SUMMARY:St Andrew’s Day
274 | UID:8a443d7e3222342742d91fa8535df82a-33@gov.uk
275 | SEQUENCE:0
276 | DTSTAMP:20191231T121333Z
277 | END:VEVENT
278 | BEGIN:VEVENT
279 | DTEND;VALUE=DATE:20181226
280 | DTSTART;VALUE=DATE:20181225
281 | SUMMARY:Christmas Day
282 | UID:8a443d7e3222342742d91fa8535df82a-34@gov.uk
283 | SEQUENCE:0
284 | DTSTAMP:20191231T121333Z
285 | END:VEVENT
286 | BEGIN:VEVENT
287 | DTEND;VALUE=DATE:20181227
288 | DTSTART;VALUE=DATE:20181226
289 | SUMMARY:Boxing Day
290 | UID:8a443d7e3222342742d91fa8535df82a-35@gov.uk
291 | SEQUENCE:0
292 | DTSTAMP:20191231T121333Z
293 | END:VEVENT
294 | BEGIN:VEVENT
295 | DTEND;VALUE=DATE:20190102
296 | DTSTART;VALUE=DATE:20190101
297 | SUMMARY:New Year’s Day
298 | UID:8a443d7e3222342742d91fa8535df82a-36@gov.uk
299 | SEQUENCE:0
300 | DTSTAMP:20191231T121333Z
301 | END:VEVENT
302 | BEGIN:VEVENT
303 | DTEND;VALUE=DATE:20190103
304 | DTSTART;VALUE=DATE:20190102
305 | SUMMARY:2nd January
306 | UID:8a443d7e3222342742d91fa8535df82a-37@gov.uk
307 | SEQUENCE:0
308 | DTSTAMP:20191231T121333Z
309 | END:VEVENT
310 | BEGIN:VEVENT
311 | DTEND;VALUE=DATE:20190420
312 | DTSTART;VALUE=DATE:20190419
313 | SUMMARY:Good Friday
314 | UID:8a443d7e3222342742d91fa8535df82a-38@gov.uk
315 | SEQUENCE:0
316 | DTSTAMP:20191231T121333Z
317 | END:VEVENT
318 | BEGIN:VEVENT
319 | DTEND;VALUE=DATE:20190507
320 | DTSTART;VALUE=DATE:20190506
321 | SUMMARY:Early May bank holiday
322 | UID:8a443d7e3222342742d91fa8535df82a-39@gov.uk
323 | SEQUENCE:0
324 | DTSTAMP:20191231T121333Z
325 | END:VEVENT
326 | BEGIN:VEVENT
327 | DTEND;VALUE=DATE:20190528
328 | DTSTART;VALUE=DATE:20190527
329 | SUMMARY:Spring bank holiday
330 | UID:8a443d7e3222342742d91fa8535df82a-40@gov.uk
331 | SEQUENCE:0
332 | DTSTAMP:20191231T121333Z
333 | END:VEVENT
334 | BEGIN:VEVENT
335 | DTEND;VALUE=DATE:20190806
336 | DTSTART;VALUE=DATE:20190805
337 | SUMMARY:Summer bank holiday
338 | UID:8a443d7e3222342742d91fa8535df82a-41@gov.uk
339 | SEQUENCE:0
340 | DTSTAMP:20191231T121333Z
341 | END:VEVENT
342 | BEGIN:VEVENT
343 | DTEND;VALUE=DATE:20191203
344 | DTSTART;VALUE=DATE:20191202
345 | SUMMARY:St Andrew’s Day
346 | UID:8a443d7e3222342742d91fa8535df82a-42@gov.uk
347 | SEQUENCE:0
348 | DTSTAMP:20191231T121333Z
349 | END:VEVENT
350 | BEGIN:VEVENT
351 | DTEND;VALUE=DATE:20191226
352 | DTSTART;VALUE=DATE:20191225
353 | SUMMARY:Christmas Day
354 | UID:8a443d7e3222342742d91fa8535df82a-43@gov.uk
355 | SEQUENCE:0
356 | DTSTAMP:20191231T121333Z
357 | END:VEVENT
358 | BEGIN:VEVENT
359 | DTEND;VALUE=DATE:20191227
360 | DTSTART;VALUE=DATE:20191226
361 | SUMMARY:Boxing Day
362 | UID:8a443d7e3222342742d91fa8535df82a-44@gov.uk
363 | SEQUENCE:0
364 | DTSTAMP:20191231T121333Z
365 | END:VEVENT
366 | BEGIN:VEVENT
367 | DTEND;VALUE=DATE:20200102
368 | DTSTART;VALUE=DATE:20200101
369 | SUMMARY:New Year’s Day
370 | UID:8a443d7e3222342742d91fa8535df82a-45@gov.uk
371 | SEQUENCE:0
372 | DTSTAMP:20191231T121333Z
373 | END:VEVENT
374 | BEGIN:VEVENT
375 | DTEND;VALUE=DATE:20200103
376 | DTSTART;VALUE=DATE:20200102
377 | SUMMARY:2nd January
378 | UID:8a443d7e3222342742d91fa8535df82a-46@gov.uk
379 | SEQUENCE:0
380 | DTSTAMP:20191231T121333Z
381 | END:VEVENT
382 | BEGIN:VEVENT
383 | DTEND;VALUE=DATE:20200411
384 | DTSTART;VALUE=DATE:20200410
385 | SUMMARY:Good Friday
386 | UID:8a443d7e3222342742d91fa8535df82a-47@gov.uk
387 | SEQUENCE:0
388 | DTSTAMP:20191231T121333Z
389 | END:VEVENT
390 | BEGIN:VEVENT
391 | DTEND;VALUE=DATE:20200509
392 | DTSTART;VALUE=DATE:20200508
393 | SUMMARY:Early May bank holiday (VE day)
394 | UID:8a443d7e3222342742d91fa8535df82a-48@gov.uk
395 | SEQUENCE:0
396 | DTSTAMP:20191231T121333Z
397 | END:VEVENT
398 | BEGIN:VEVENT
399 | DTEND;VALUE=DATE:20200526
400 | DTSTART;VALUE=DATE:20200525
401 | SUMMARY:Spring bank holiday
402 | UID:8a443d7e3222342742d91fa8535df82a-49@gov.uk
403 | SEQUENCE:0
404 | DTSTAMP:20191231T121333Z
405 | END:VEVENT
406 | BEGIN:VEVENT
407 | DTEND;VALUE=DATE:20200804
408 | DTSTART;VALUE=DATE:20200803
409 | SUMMARY:Summer bank holiday
410 | UID:8a443d7e3222342742d91fa8535df82a-50@gov.uk
411 | SEQUENCE:0
412 | DTSTAMP:20191231T121333Z
413 | END:VEVENT
414 | BEGIN:VEVENT
415 | DTEND;VALUE=DATE:20201201
416 | DTSTART;VALUE=DATE:20201130
417 | SUMMARY:St Andrew’s Day
418 | UID:8a443d7e3222342742d91fa8535df82a-51@gov.uk
419 | SEQUENCE:0
420 | DTSTAMP:20191231T121333Z
421 | END:VEVENT
422 | BEGIN:VEVENT
423 | DTEND;VALUE=DATE:20201226
424 | DTSTART;VALUE=DATE:20201225
425 | SUMMARY:Christmas Day
426 | UID:8a443d7e3222342742d91fa8535df82a-52@gov.uk
427 | SEQUENCE:0
428 | DTSTAMP:20191231T121333Z
429 | END:VEVENT
430 | BEGIN:VEVENT
431 | DTEND;VALUE=DATE:20201229
432 | DTSTART;VALUE=DATE:20201228
433 | SUMMARY:Boxing Day
434 | UID:8a443d7e3222342742d91fa8535df82a-53@gov.uk
435 | SEQUENCE:0
436 | DTSTAMP:20191231T121333Z
437 | END:VEVENT
438 | BEGIN:VEVENT
439 | DTEND;VALUE=DATE:20210102
440 | DTSTART;VALUE=DATE:20210101
441 | SUMMARY:New Year’s Day
442 | UID:8a443d7e3222342742d91fa8535df82a-54@gov.uk
443 | SEQUENCE:0
444 | DTSTAMP:20191231T121333Z
445 | END:VEVENT
446 | BEGIN:VEVENT
447 | DTEND;VALUE=DATE:20210105
448 | DTSTART;VALUE=DATE:20210104
449 | SUMMARY:2nd January
450 | UID:8a443d7e3222342742d91fa8535df82a-55@gov.uk
451 | SEQUENCE:0
452 | DTSTAMP:20191231T121333Z
453 | END:VEVENT
454 | BEGIN:VEVENT
455 | DTEND;VALUE=DATE:20210403
456 | DTSTART;VALUE=DATE:20210402
457 | SUMMARY:Good Friday
458 | UID:8a443d7e3222342742d91fa8535df82a-56@gov.uk
459 | SEQUENCE:0
460 | DTSTAMP:20191231T121333Z
461 | END:VEVENT
462 | BEGIN:VEVENT
463 | DTEND;VALUE=DATE:20210504
464 | DTSTART;VALUE=DATE:20210503
465 | SUMMARY:Early May bank holiday
466 | UID:8a443d7e3222342742d91fa8535df82a-57@gov.uk
467 | SEQUENCE:0
468 | DTSTAMP:20191231T121333Z
469 | END:VEVENT
470 | BEGIN:VEVENT
471 | DTEND;VALUE=DATE:20210601
472 | DTSTART;VALUE=DATE:20210531
473 | SUMMARY:Spring bank holiday
474 | UID:8a443d7e3222342742d91fa8535df82a-58@gov.uk
475 | SEQUENCE:0
476 | DTSTAMP:20191231T121333Z
477 | END:VEVENT
478 | BEGIN:VEVENT
479 | DTEND;VALUE=DATE:20210803
480 | DTSTART;VALUE=DATE:20210802
481 | SUMMARY:Summer bank holiday
482 | UID:8a443d7e3222342742d91fa8535df82a-59@gov.uk
483 | SEQUENCE:0
484 | DTSTAMP:20191231T121333Z
485 | END:VEVENT
486 | BEGIN:VEVENT
487 | DTEND;VALUE=DATE:20211201
488 | DTSTART;VALUE=DATE:20211130
489 | SUMMARY:St Andrew’s Day
490 | UID:8a443d7e3222342742d91fa8535df82a-60@gov.uk
491 | SEQUENCE:0
492 | DTSTAMP:20191231T121333Z
493 | END:VEVENT
494 | BEGIN:VEVENT
495 | DTEND;VALUE=DATE:20211228
496 | DTSTART;VALUE=DATE:20211227
497 | SUMMARY:Christmas Day
498 | UID:8a443d7e3222342742d91fa8535df82a-61@gov.uk
499 | SEQUENCE:0
500 | DTSTAMP:20191231T121333Z
501 | END:VEVENT
502 | BEGIN:VEVENT
503 | DTEND;VALUE=DATE:20211229
504 | DTSTART;VALUE=DATE:20211228
505 | SUMMARY:Boxing Day
506 | UID:8a443d7e3222342742d91fa8535df82a-62@gov.uk
507 | SEQUENCE:0
508 | DTSTAMP:20191231T121333Z
509 | END:VEVENT
510 | END:VCALENDAR
511 |
--------------------------------------------------------------------------------
/src/tick/ical.clj:
--------------------------------------------------------------------------------
1 | ;; Copyright © 2016-2018, JUXT LTD.
2 |
3 | (ns tick.ical
4 | (:require
5 | [clojure.set :as set]
6 | [clojure.string :as str]
7 | [clojure.java.io :as io]
8 | [clojure.spec.alpha :as s]
9 | [tick.core :as t]
10 | [tick.interval :as ival])
11 | (:import
12 | [java.time Instant LocalDate LocalDateTime ZonedDateTime ZoneId ZoneRegion]
13 | [java.time.format DateTimeFormatter]))
14 |
15 | ;; Have considered wrapping ical4j. However, since ical4j does not
16 | ;; use Java 8 time (as of the time of writing), any appeals to
17 | ;; maturity/stability are moot. Sometimes it's a bad idea to wrap a
18 | ;; Java library simply because one exists (concurrency issues, etc.)
19 |
20 | (def CRLF "\r\n")
21 |
22 | (defn contentline
23 | "Fold the given string value returning a sequence of strings up to
24 | 75 octets in length, as per RFC 5545 folding rules."
25 | [s]
26 | (let [limit 75]
27 | (loop [acc [] n 0]
28 | (if (>= (+ n limit) (count s))
29 | (conj acc (str
30 | (if (empty? acc) "" " ")
31 | (subs s n)
32 | CRLF))
33 | (recur
34 | (conj acc (str
35 | (if (empty? acc) "" " ")
36 | (subs s n (+ n limit))
37 | CRLF))
38 | (+ limit n))))))
39 |
40 | (defn print-cl
41 | "Folding print, where long lines are folded as per RFC 5545 Section 4.1"
42 | [& args]
43 | (doseq [line (contentline (apply str args))]
44 | (print line)))
45 |
46 | (defprotocol ICalendarValue
47 | (serialize-value [_] ""))
48 |
49 | (def DATE-TIME-FORM-1-PATTERN (DateTimeFormatter/ofPattern "YYYYMMdd'T'HHmmss"))
50 |
51 | ;; Only call with ZoneID of UTC otherwise produces invalid ICAL format
52 | (def DATE-TIME-FORM-2-PATTERN (DateTimeFormatter/ofPattern "YYYYMMdd'T'HHmmssX"))
53 |
54 | (def DATE-TIME-FORM-3-PATTERN (DateTimeFormatter/ofPattern "YYYYMMdd'T'HHmmss"))
55 |
56 | (extend-protocol ICalendarValue
57 | String
58 | (serialize-value [s] {:value s})
59 | Instant
60 | (serialize-value [i] {:value (.format (ZonedDateTime/ofInstant i (ZoneId/of "UTC")) DATE-TIME-FORM-2-PATTERN)})
61 | LocalDate
62 | (serialize-value [s] {:value (.format s DateTimeFormatter/BASIC_ISO_DATE)})
63 | LocalDateTime
64 | (serialize-value [s] {:value (.format s DATE-TIME-FORM-1-PATTERN)})
65 | ZonedDateTime
66 | (serialize-value [s] {:value (.format s DATE-TIME-FORM-3-PATTERN)
67 | :params {:tzid (t/zone s)}})
68 | ZoneRegion
69 | (serialize-value [zr] {:value (str zr)})
70 | clojure.lang.APersistentMap
71 | (serialize-value [s] s))
72 |
73 | (defprotocol ICalendarObject
74 | (property-values [obj prop-name]
75 | "Return the properties of an ICalendarObject with the given
76 | name. Returns a sequence of values, since iCalendar properties may
77 | have multiple values.")
78 | (property-value [obj prop-name]
79 | "Return the first property value of an ICalendarObject."))
80 |
81 | (defprotocol IPrintable
82 | (print-object [_] "Print as an iCalendar object"))
83 |
84 | (defmacro wrap-with [c & body]
85 | `(do
86 | (print-cl "BEGIN:" ~c)
87 | ~@body
88 | (print-cl "END:" ~c)))
89 |
90 | (defn print-property [prop-name prop-value]
91 | (let [{:keys [params value]} (serialize-value prop-value)]
92 | (print-cl
93 | (str/upper-case (name prop-name))
94 | (apply str (for [[k v] params]
95 | (str ";" (str/upper-case (name k)) "=" v)))
96 | ":"
97 | value)))
98 |
99 | (defrecord Property [prop-name prop-value]
100 | IPrintable
101 | (print-object [_]
102 | (print-property prop-name prop-value)))
103 |
104 | (defrecord VEvent []
105 | t/ITimeSpan
106 | (beginning [this] (t/beginning (property-value this :dtstart)))
107 | ;; This might seem wrong but we use t/beginning to convert a
108 | ;; date-time to the same date-time, and a date to midnight (the
109 | ;; start of that date). TODO: Is this explained in the RFC anywhere?
110 | (end [this] (t/beginning (property-value this :dtend)))
111 |
112 | IPrintable
113 | (print-object [this]
114 | (wrap-with
115 | "VEVENT"
116 | (doseq [prop (:properties this)]
117 | (print-object prop))))
118 |
119 | ICalendarObject
120 | (property-values [this prop-name]
121 | (->> this
122 | :properties
123 | (filter #(= (:name %) (str/upper-case (name prop-name))))
124 | (mapv :value)))
125 | (property-value [this prop-name]
126 | (first (property-values this prop-name)))
127 |
128 | t/IConversion
129 | (inst [this] (t/inst (property-value this :dtstart)))
130 | (instant [this] (t/inst (property-value this :dtstart)))
131 | (offset-date-time [this] (t/offset-date-time (property-value this :dtstart)))
132 | (zoned-date-time [this] (t/zoned-date-time (property-value this :dtstart)))
133 |
134 | t/IExtraction
135 | (time [this] (t/time (property-value this :dtstart)))
136 | (date [this] (t/date (property-value this :dtstart)))
137 | (date-time [this] (t/date-time (property-value this :dtstart)))
138 | (nanosecond [this] (t/nanosecond (property-value this :dtstart)))
139 | (microsecond [this] (t/microsecond (property-value this :dtstart)))
140 | (millisecond [this] (t/millisecond (property-value this :dtstart)))
141 | (second [this] (t/second (property-value this :dtstart)))
142 | (minute [this] (t/minute (property-value this :dtstart)))
143 | (hour [this] (t/hour (property-value this :dtstart)))
144 | (day-of-week [this] (t/day-of-week (property-value this :dtstart)))
145 | (day-of-month [this] (t/day-of-month (property-value this :dtstart)))
146 | (month [this] (t/month (property-value this :dtstart)))
147 | (year [this] (t/year (property-value this :dtstart)))
148 | (year-month [this] (t/year-month (property-value this :dtstart)))
149 | (zone [this] (t/zone (property-value this :dtstart)))
150 | (zone-offset [this] (t/zone-offset (property-value this :dtstart))))
151 |
152 | (defrecord VCalendar [objects]
153 | ;; TODO: Add t/ITimeSpan
154 | IPrintable
155 | (print-object [this]
156 | (wrap-with
157 | "VCALENDAR"
158 | (doseq [obj objects]
159 | (print-object obj))))
160 | ICalendarObject
161 | (property-values [this prop-name]
162 | (->> this
163 | :properties
164 | (filter #(= (:name %) (str/upper-case (name prop-name))))
165 | (mapv :value)))
166 | (property-value [this prop-name]
167 | (first (property-values this prop-name))))
168 |
169 | (comment
170 | ;; Form 1: DATE WITH LOCAL TIME
171 | (with-out-str
172 | (print-object
173 | (vcalendar
174 | (vevent "Malcolm is on holiday!"
175 | (t/date "2018-07-21")
176 | (t/date "2018-07-31")
177 | :description "The content information associated with an iCalendar object is formatted using a syntax similar to that defined by [RFC 2425]. That is, the content information consists of CRLF-separated content lines."))))
178 |
179 | ;; Form 2: DATE WITH UTC TIME
180 | (with-out-str
181 | (print-object
182 | (vcalendar
183 | (vevent "Malcolm is in a meeting!"
184 | (t/now)
185 | (t/+ (t/now) (t/minutes 50))
186 | :description "The content information associated with an iCalendar object is formatted using a syntax similar to that defined by [RFC 2425]. That is, the content information consists of CRLF-separated content lines."))))
187 |
188 | ;; Form 3: DATE WITH LOCAL TIME AND TIME ZONE REFERENCE
189 | (with-out-str
190 | (print-object
191 | (vcalendar
192 | (vevent
193 | "Malcolm is in a meeting, in New York!"
194 | (t/at-zone (t/at (t/today) "14:00") "America/New_York")
195 | (t/at-zone (t/at (t/today) "14:50") "America/New_York")
196 | :description "The content information associated with an iCalendar object is formatted using a syntax similar to that defined by [RFC 2425]. That is, the content information consists of CRLF-separated content lines.")))))
197 |
198 |
199 | ;; Parsing with spec
200 |
201 | (s/def ::contentline
202 | (s/cat
203 | :name ::name
204 | :params (s/*
205 | (s/cat
206 | :semicolon #{\;}
207 | :param ::param))
208 | :colon #{\:}
209 | :value ::value))
210 |
211 | (defn char-range [from to]
212 | (map char (range (int from) (inc (int to)))))
213 |
214 | (def QSAFE-CHAR
215 | (set (concat [\space \t]
216 | [(char 0x21)]
217 | (char-range 0x23 0x7e))))
218 |
219 | (def SAFE-CHAR
220 | (set (concat [\space \t]
221 | [(char 0x21)]
222 | (char-range 0x23 0x2b)
223 | (char-range 0x2d 0x39)
224 | (char-range 0x3c 0x7e))))
225 |
226 | (def VALUE-CHAR
227 | (set (concat [\space \t]
228 | (char-range 0x21 0x7e))))
229 | ;; TODO: Add NON-US-ASCII
230 |
231 | (def CONTROL
232 | (set (concat (char-range 0x00 0x08)
233 | (char-range 0x0a 0x1f)
234 | [0x7f])))
235 |
236 | (s/def ::name
237 | (s/alt :iana-token ::iana-token
238 | :x-name ::x-name))
239 |
240 | (def ALPHA-DIGIT
241 | (set/union (set (char-range \a \z))
242 | (set (char-range \A \Z))
243 | (set (char-range \0 \9))))
244 |
245 | (s/def ::iana-token
246 | (s/+ (conj ALPHA-DIGIT #{\-})))
247 |
248 | (s/def ::x-name
249 | (s/cat
250 | :prefix (s/cat :x1 #{\X}
251 | :x2 #{\-})
252 | :vendorid (s/? (s/cat :vendorid ::vendorid
253 | :dash #{\-}))
254 | :suffix (s/+ (set "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890-"))))
255 |
256 | (s/def ::vendorid
257 | (s/and
258 | (s/+ (set "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890"))
259 | #(= (count %) 3)))
260 |
261 | (s/def ::param
262 | (s/cat :param-name ::param-name
263 | :equals #{\=}
264 | :param-value ::param-value
265 | :param-values (s/* (s/cat :comma #{,} :param-value ::param-value))))
266 |
267 | (s/def ::param-name
268 | (s/alt :iana-token ::iana-token :x-name ::x-name))
269 |
270 | (s/def ::param-value
271 | (s/alt :paramtext ::paramtext
272 | :quoted-string ::quoted-string))
273 |
274 | (s/def ::paramtext (s/* ::SAFE-CHAR))
275 |
276 | (s/def ::value (s/* ::VALUE-CHAR))
277 |
278 | (s/def ::quoted-string
279 | (s/cat
280 | :open-quote #{\"}
281 | :content (s/* ::QSAFE-CHAR)
282 | :close-quote #{\"}))
283 |
284 | ;; Any character except CONTROL and DQUOTE
285 | (s/def ::QSAFE-CHAR (comp not (conj CONTROL \")))
286 |
287 | ;; Any character except CONTROL, DQUOTE, ";", ":", ","
288 | (s/def ::SAFE-CHAR (comp not (set/union CONTROL #{\" \; \: \,})))
289 |
290 | (s/def ::VALUE-CHAR (comp not CONTROL))
291 |
292 | (defn unfolding-line-seq*
293 | [^java.io.BufferedReader rdr hold]
294 | (if-let [line (.readLine rdr)]
295 | (if (= (.charAt line 0) \space)
296 | (recur rdr (conj hold line))
297 | (cons (str/join hold) (lazy-seq (unfolding-line-seq* rdr [line]))))
298 | [(str/join hold)]))
299 |
300 | (defn unfolding-line-seq [^java.io.BufferedReader rdr]
301 | (next (unfolding-line-seq* rdr [])))
302 |
303 | (defn extract-name-as-string [[k v]]
304 | (case k
305 | :iana-token (apply str v)
306 | :x-name (str "X-" (apply str (:suffix v)))
307 | (throw (ex-info "Bad input" {:k k}))))
308 |
309 | (defn extract-param-value-as-string [[k v]]
310 | (case k
311 | :paramtext (apply str v)
312 | :quoted-string (apply str (:content v))))
313 |
314 | (defn line->contentline [s]
315 | (let [m (s/conform ::contentline (seq s))]
316 | (when-not (:name m) (throw (ex-info "No name" {:contentline s})))
317 | (let [str-value (-> m :value str/join)]
318 | {:name (-> m :name extract-name-as-string)
319 | :params (->> m :params
320 | (map (juxt
321 | (comp extract-name-as-string :param-name :param)
322 | (comp extract-param-value-as-string :param-value :param)))
323 | (into {} ))
324 | ;; We set to the string, but properties may replace this with
325 | ;; another type
326 | :value str-value
327 | ;; We always retain the original string value
328 | :string-value str-value})))
329 |
330 | ;; JCF tip
331 | ;;(s/conform (s/and (s/conformer seq) ::iana-token) "foobar")
332 |
333 | (defmulti add-contentline-to-model
334 | "A reducing function that gives a parsed content-line to an
335 | accumulator that builds a model."
336 | (fn [acc cl] (:name cl)))
337 |
338 | (defn error [acc contentline message]
339 | (update acc :errors
340 | (fnil conj [])
341 | {:error message
342 | :lineno (:lineno contentline)}))
343 |
344 | (defn- instantiate [m]
345 | (case (:object m)
346 | "VCALENDAR" (map->VCalendar m)
347 | "VEVENT" (map->VEvent m)
348 | m))
349 |
350 | (defmethod add-contentline-to-model "BEGIN"
351 | [acc contentline]
352 | ;; BEGIN means place the current object in the stack, and start a
353 | ;; new one
354 | (cond-> acc
355 | (:curr-object acc) (update :stack (fnil conj []) (:curr-object acc))
356 | true (assoc :curr-object {:object (:value contentline)
357 | :lineno (:lineno contentline)})))
358 |
359 | (defmethod add-contentline-to-model "END"
360 | [acc contentline]
361 | ;; END means place the current object in the stack, and start a new
362 | ;; one
363 | (let [curr-object (instantiate (:curr-object acc))
364 | restore-object (some-> (:stack acc) last) ; check if nil, bad state
365 | restore-object (update restore-object :subobjects (fnil conj []) curr-object)]
366 | (-> acc
367 | (assoc :curr-object restore-object
368 | :stack (vec (butlast (:stack acc)))))))
369 |
370 | (defmulti coerce-to-value (fn [value-type value] value-type))
371 | (defmethod coerce-to-value "DATE" [_ value] (LocalDate/parse value DateTimeFormatter/BASIC_ISO_DATE))
372 | (defmethod coerce-to-value nil [_ value] value)
373 |
374 | (defn add-property [acc contentline]
375 | (update-in
376 | acc [:curr-object :properties] (fnil conj [])
377 | (update contentline
378 | :value
379 | #(coerce-to-value (get-in contentline [:params "VALUE"]) %))))
380 |
381 | (defmethod add-contentline-to-model :default
382 | [acc contentline]
383 | (add-property acc contentline))
384 |
385 | (defn parse-ical [^java.io.BufferedReader r]
386 | (->> r
387 | unfolding-line-seq
388 | (map line->contentline)
389 | (map-indexed (fn [n o] (assoc o :lineno (inc n))))
390 | (reduce add-contentline-to-model {})
391 | :curr-object
392 | :subobjects))
393 |
394 | (defn events
395 | "Given a vcalendar object, return only the events"
396 | [vcalendar]
397 | (filter #(= "VEVENT" (:object %)) (:subobjects vcalendar)))
398 |
--------------------------------------------------------------------------------