├── 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 |
10 | 11 | 12 | 13 |
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 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 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 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 17 | 24 | 25 | 27 | 28 | 29 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /docs/logo-normal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 17 | 24 | 25 | 27 | 28 | 29 | 32 | 33 | 34 | 35 | 36 | 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 | 20 | 22 | 25 | 29 | 33 | 34 | 43 | 44 | 62 | 64 | 65 | 67 | image/svg+xml 68 | 70 | 71 | 72 | 73 | 74 | 78 | 85 | 92 | 27 103 | Wednesday 115 | FEBRUARY 126 | 127 | 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 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /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 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 30 46 | 47 | 48 | 60 49 | 50 | 51 | 15 52 | 53 | 54 | 45 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 60 65 | 66 | 67 | 30 68 | 69 | 70 | 15 71 | 72 | 73 | 45 74 | 75 | 76 | 77 | 78 | 79 | 80 | 5 81 | 82 | 83 | 10 84 | 85 | 86 | 20 87 | 88 | 89 | 25 90 | 91 | 92 | 35 93 | 94 | 95 | 40 96 | 97 | 98 | 50 99 | 100 | 101 | 55 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | MADE IN ITALY 113 | 114 | 115 | 116 | 117 | 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 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 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 | --------------------------------------------------------------------------------