Skip to content

Commit

Permalink
Polish 🚿
Browse files Browse the repository at this point in the history
  • Loading branch information
camsaul committed Apr 20, 2020
1 parent f64ec4a commit 3cda9c8
Show file tree
Hide file tree
Showing 9 changed files with 204 additions and 92 deletions.
7 changes: 0 additions & 7 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,20 +132,13 @@ workflows:
- deps
lein-command: bikeshed

- lein:
name: yagni
requires:
- deps
lein-command: yagni

- lein:
name: deploy
requires:
- bikeshed
- docstring-checker
- eastwood
- namespace-decls
- yagni
- tests
- tests-java-11
lein-command: deploy clojars
Expand Down
92 changes: 90 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,94 @@

[![Clojars Project](https://clojars.org/metabase/second-date/latest-version.svg)](http://clojars.org/metabase/second-date)

# Cam's Clojure Library template
# Second Date

Run `./init.sh` and follow the prompts. That's it!
Second Date provides a handful of utility functions for working with `java.time`. The most important are:

###### `parse`

Can parse almost any temporal literal String to the correct `java.time` class.

```clj
(require '[second-date.core :as second-date])

(second-date/parse "2020-04")
;; -> #object[java.time.LocalDate 0x1998e54f "2020-04-01"]

(second-date/parse "2020-04-01")
;; -> #object[java.time.LocalDate 0x1998e54f "2020-04-01"]

(second-date/parse "2020-04-01T15:01")
;; -> #object[java.time.LocalDateTime 0x121829b7 "2020-04-01T15:01"]

(second-date/parse "2020-04-01T15:01-07:00")
;; -> #object[java.time.OffsetDateTime 0x7dc126b0 "2020-04-01T15:01-07:00"]

(second-date/parse "2020-04-01T15:01-07:00[US/Pacific]")
;; -> #object[java.time.ZonedDateTime 0x351fb7c8 "2020-04-01T15:01-07:00[US/Pacific]"]
```

`parse` handles ISO-8601 strings as well as SQL literals (e.g. `2020-04-01 15:01:00`).

###### `format`

Formats any of the main `java.time` temporal instant classes as a String. Uses ISO-8601 by default, but can use any
`java.time.format.DateTimeFormatter` or keywords naming static formatters as understood by
[`clojure.java-time`](https://github.com/dm3/clojure.java-time).

```clj
(require '[java-time :as t]
'[second-date.core :as second-date])

(second-date/format (t/zoned-date-time "2020-04-01T15:01-07:00[US/Pacific]"))
;; -> "2020-04-01T16:01:00-07:00"

(second-date/format (t/offset-date-time "2020-04-01T15:01-07:00"))
;; -> "2020-04-01T16:01:00-07:00"

(second-date/format (t/local-date-time "2020-04-01T15:01"))
;; -> "2020-04-01T15:01:00"

;; with a different formatter
(second-date/format :basic-iso-date (t/local-date-time "2020-04-01T15:01"))
;; -> "20200401"

;; it even handles Instants
(second-date/format :iso-week-date (t/instant "2020-04-01T15:01:00-07:00"))
;; "2020-W14-3Z"
```

Check the value of `java-time.format/predefined-formatters` for all supported predefined formatters.

###### `second-date.parse.builder/formatter`

`second-date.parse.builder/formatter` is a Clojure interface for `java.time.format.DateTimeFormatterBuilder`, used to
create `DateTimeFormatter`s.

```clj
(require '[java-time :as t]
'[second-date :as second-date]
'[second-date.parse.builder :as b])

(def my-time-formatter
(b/formatter
(b/case-insensitive
(b/value :hour-of-day 2)
(b/optional
":"
(b/value :minute-of-hour 2)
(b/optional
":"
(b/value :second-of-minute))))))

;; -> #object[java.time.format.DateTimeFormatter "ParseCaseSensitive(false)Value(HourOfDay,2)[':'Value(MinuteOfHour,2)[':'Value(SecondOfMinute)]]"]

;; you can now use the formatter to format and parse
(second-date/format my-time-formatter (t/zoned-date-time "2020-04-01T15:23:52.878132-07:00[America/Los_Angeles]"))
;; -> "15:23:52"

(second-date/parse my-time-formatter "15:23:52")
;; -> #object[java.time.LocalTime 0x514c293c "15:23:52"]
```

Some example formatters can be found in `second-date.parse`.
23 changes: 9 additions & 14 deletions project.clj
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,13 @@

:aliases
{"test" ["with-profile" "+test" "test"]
"bikeshed" ["with-profile" "+bikeshed" "bikeshed" "--max-line-length" "120"]
"bikeshed" ["with-profile" "+bikeshed" "bikeshed" "--max-line-length" "200"]
"check-namespace-decls" ["with-profile" "+check-namespace-decls" "check-namespace-decls"]
"eastwood" ["with-profile" "+eastwood" "eastwood"]
"check-reflection-warnings" ["with-profile" "+reflection-warnings" "check"]
"docstring-checker" ["with-profile" "+docstring-checker" "docstring-checker"]
"yagni" ["with-profile" "+yagni" "yagni"]
;; `lein lint` will run all linters
"lint" ["do" ["eastwood"] ["bikeshed"] ["yagni"] ["check-namespace-decls"] ["docstring-checker"]]}
"lint" ["do" ["eastwood"] ["bikeshed"] ["check-namespace-decls"] ["docstring-checker"]]}

:dependencies
[[clojure.java-time "0.3.2"]
Expand All @@ -33,14 +32,14 @@
{:plugins
[[jonase/eastwood "0.3.11" :exclusions [org.clojure/clojure]]]

:add-linters
[:unused-private-vars
:unused-namespaces
:unused-fn-args
:unused-locals]
:eastwood
{:add-linters
[:unused-private-vars
:unused-fn-args
:unused-locals]

:exclude-linters
[:deprecations]}
:exclude-linters
[:implicit-dependencies]}}

:docstring-checker
{:plugins
Expand All @@ -53,10 +52,6 @@
{:plugins
[[lein-bikeshed "0.5.2"]]}

:yagni
{:plugins
[[venantius/yagni "0.1.7"]]}

:check-namespace-decls
{:plugins [[lein-check-namespace-decls "1.0.2"]]
:source-paths ["test"]
Expand Down
2 changes: 0 additions & 2 deletions src/second_date/common.clj
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
(:require [clojure.string :as str])
(:import [java.time.temporal ChronoField IsoFields TemporalField WeekFields]))

;; TODO - not sure this belongs here, it seems to be a bit more general than just `date-2`.

(defn static-instances
"Utility function to get the static members of a class. Returns map of `lisp-case` keyword names of members -> value."
([^Class klass]
Expand Down
66 changes: 47 additions & 19 deletions src/second_date/core.clj
Original file line number Diff line number Diff line change
Expand Up @@ -27,26 +27,54 @@
OffsetDateTime :iso-offset-date-time
ZonedDateTime :iso-offset-date-time))

(defn- format* [formatter t]
(when t
(if (t/instant? t)
(recur formatter (t/zoned-date-time t (t/zone-id "UTC")))
(t/format formatter t))))

(defn format
"Format temporal value `t` as a ISO-8601 date/time/datetime string."
^String [t]
(when t
(format* (temporal->iso-8601-formatter t) t)))
"Format any of the main `java.time` temporal instant classes as a String. Uses ISO-8601 by default, but `formatter`
can be a `java.time.format.DateTimeFormatter` or keywords naming static formatters as understood by
[`clojure.java-time`](https://github.com/dm3/clojure.java-time). Check the value of
`java-time.format/predefined-formatters` for all supported predefined formatters.
(second-date/format (t/zoned-date-time \"2020-04-01T15:01-07:00[US/Pacific]\"))
;; -> \"2020-04-01T16:01:00-07:00\"
(second-date/format (t/offset-date-time \"2020-04-01T15:01-07:00\"))
;; -> \"2020-04-01T16:01:00-07:00\"
(second-date/format (t/local-date-time \"2020-04-01T15:01\"))
;; -> \"2020-04-01T15:01:00\"
;; with a different formatter
(second-date/format :basic-iso-date (t/local-date-time \"2020-04-01T15:01\"))
;; -> \"20200401\"
;; it even handles Instants
(second-date/format :iso-week-date (t/instant \"2020-04-01T15:01:00-07:00\"))
;; \"2020-W14-3Z\""
(^String [t]
(when t
(format (temporal->iso-8601-formatter t) t)))

(^String [formatter t]
(when t
(if (t/instant? t)
(recur formatter (t/zoned-date-time t (t/zone-id "UTC")))
(t/format formatter t)))))

;; replace the `T` with a space. Easy!
(defn- replace-T-with-space [s]
(when s
(str/replace-first s #"(\d{2})T(\d{2})" "$1 $2")))

(defn format-sql
"Format a temporal value `t` as a SQL-style literal string (for most SQL databases). This is the same as ISO-8601 but
uses a space rather than of a `T` to separate the date and time components."
^String [t]
;; replace the `T` with a space. Easy!
(str/replace-first (format t) #"(\d{2})T(\d{2})" "$1 $2"))
(^String [t]
(replace-T-with-space (format t)))

(^String [formatter t]
(replace-T-with-space (format formatter t))))

(def ^:private add-units
(def add-units
"Units supported by the `add` function."
#{:millisecond :second :minute :hour :day :week :month :quarter :year})

(defn add
Expand Down Expand Up @@ -76,7 +104,7 @@
;; TIMEZONE FIXME - we should add `:millisecond-of-second` (or `:fraction-of-second`?) and `:second-of-minute` as
;; well. Not sure where we'd use these, but we should have them for consistency
(def extract-units
"Units which return a (numerical, periodic) component of a date"
"Units which return a (numerical, periodic) component of a date."
#{:minute-of-hour
:hour-of-day
:day-of-week
Expand All @@ -87,8 +115,8 @@
:iso-week-of-year
:month-of-year
:quarter-of-year
;; TODO - in this namespace `:year` is something you can both extract and truncate to. In MBQL `:year` is a truncation
;; operation. Maybe we should rename this unit to clear up the potential confusion (?)
;; TODO - in this namespace `:year` is something you can both extract and truncate to. In MBQL `:year` is a
;; truncation operation. Maybe we should rename this unit to clear up the potential confusion (?)
:year})

(def ^:private week-fields*
Expand Down Expand Up @@ -134,7 +162,7 @@

(defmethod adjuster :default
[k]
(throw (ex-info (format "No temporal adjuster named %s" k) {})))
(throw (ex-info (clojure.core/format "No temporal adjuster named %s" k) {})))

(defmethod adjuster :first-day-of-week
[_]
Expand Down Expand Up @@ -221,7 +249,7 @@
(= unit :default) t
(extract-units unit) (extract t unit)
(truncate-units unit) (truncate t unit)
:else (throw (ex-info (format "Invalid unit: %s" unit) {:unit unit})))))
:else (throw (ex-info (clojure.core/format "Invalid unit: %s" unit) {:unit unit})))))

(defn range
"Get a start (by default, inclusive) and end (by default, exclusive) pair of instants for a `unit` span of time
Expand Down
26 changes: 22 additions & 4 deletions src/second_date/parse.clj
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@
(b/optional
(b/zone-id))))

(def ^:private ^DateTimeFormatter formatter
(def ^:private ^DateTimeFormatter default-formatter
(b/formatter
(b/case-insensitive
(b/optional
Expand All @@ -121,6 +121,24 @@
offset-formatter*))))

(defn parse
"Parse a string into a `java.time` object."
[^String s]
(parse-with-formatter formatter s))
"Parse almost any temporal literal String to a `java.time` object.
(second-date/parse \"2020-04\")
;; -> #object[java.time.LocalDate 0x1998e54f \"2020-04-01\"]
(second-date/parse \"2020-04-01\")
;; -> #object[java.time.LocalDate 0x1998e54f \"2020-04-01\"]
(second-date/parse \"2020-04-01T15:01\")
;; -> #object[java.time.LocalDateTime 0x121829b7 \"2020-04-01T15:01\"]
(second-date/parse \"2020-04-01T15:01-07:00\")
;; -> #object[java.time.OffsetDateTime 0x7dc126b0 \"2020-04-01T15:01-07:00\"]
(second-date/parse \"2020-04-01T15:01-07:00[US/Pacific]\")
;; -> #object[java.time.ZonedDateTime 0x351fb7c8 \"2020-04-01T15:01-07:00[US/Pacific]\"]"
(^Temporal [s]
(parse-with-formatter default-formatter s))

(^Temporal [formatter s]
(parse-with-formatter formatter s)))
6 changes: 3 additions & 3 deletions src/second_date/parse/builder.clj
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@

(defn value
"Define a section for a specific field such as `:hour-of-day` or `:minute-of-hour`. Refer to
`metabase.util.date-2.common/temporal-field` for all possible temporal fields names."
`second-date.common/temporal-field` for all possible temporal fields names."
([temporal-field-name]
(fn [^DateTimeFormatterBuilder builder]
(.appendValue builder (temporal-field temporal-field-name))))
Expand All @@ -122,7 +122,7 @@
"Define a section for a fractional value, e.g. milliseconds or nanoseconds."
[temporal-field-name min-val-width max-val-width & {:keys [decimal-point?]}]
(fn [^DateTimeFormatterBuilder builder]
(.appendFraction builder (temporal-field temporal-field-name) 0 9 (boolean decimal-point?))))
(.appendFraction builder (temporal-field temporal-field-name) min-val-width max-val-width (boolean decimal-point?))))

(defn zone-offset
"Define a section for a timezone offset. e.g. `-08:00`."
Expand All @@ -142,7 +142,7 @@
(optional "]"))))

(defn formatter
"Return a new `DateTimeFormatter` from `sections`. See examples in `metabase.util.date-2.parse` for more details.
"Return a new `DateTimeFormatter` from `sections`. See examples in `second-date.parse` for more details.
(formatter
(case-insensitive
Expand Down
11 changes: 7 additions & 4 deletions test/second_date/core_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
(format "Extract %s from %s %s should be %s" unit (class t) t expected)))))
(testing "second-date/extract with 1 arg (extract from now)"
(is (= 2
(t/with-clock (t/mock-clock (t/instant "2019-11-18T22:31:00Z"))
(t/with-clock (t/mock-clock (t/instant "2019-11-18T22:31:00Z") (t/zone-id "UTC"))
(second-date/extract :day-of-week))))))

(deftest truncate-test
Expand Down Expand Up @@ -127,14 +127,15 @@
(format "Truncate %s %s to %s should be %s" (class t) t unit expected)))))
(testing "second-date/truncate with 1 arg (truncate now)"
(is (= (t/zoned-date-time "2019-11-18T00:00Z[UTC]")
(t/with-clock (t/mock-clock (t/instant "2019-11-18T22:31:00Z"))
(t/with-clock (t/mock-clock (t/instant "2019-11-18T22:31:00Z") (t/zone-id "UTC"))
(second-date/truncate :day))))))

(deftest add-test
(testing "with 2 args (datetime relative to now)"
(is (= (t/zoned-date-time "2019-11-20T22:31Z[UTC]")
(t/with-clock (t/mock-clock (t/instant "2019-11-18T22:31:00Z"))
(t/with-clock (t/mock-clock (t/instant "2019-11-18T22:31:00Z") (t/zone-id "UTC"))
(second-date/add :day 2)))))

(testing "with 3 args"
(let [t (t/zoned-date-time "2019-06-14T00:00:00.000Z[UTC]")]
(doseq [[unit n expected] [[:second 5 "2019-06-14T00:00:05Z[UTC]"]
Expand All @@ -153,12 +154,14 @@
(testing "with 1 arg (range relative to now)"
(is (= {:start (t/zoned-date-time "2019-11-17T00:00Z[UTC]")
:end (t/zoned-date-time "2019-11-24T00:00Z[UTC]")}
(t/with-clock (t/mock-clock (t/instant "2019-11-18T22:31:00Z"))
(t/with-clock (t/mock-clock (t/instant "2019-11-18T22:31:00Z") (t/zone-id "UTC"))
(second-date/range :week)))))

(testing "with 2 args"
(is (= {:start (t/zoned-date-time "2019-10-27T00:00Z[UTC]")
:end (t/zoned-date-time "2019-11-03T00:00Z[UTC]")}
(second-date/range (t/zoned-date-time "2019-11-01T15:29:00Z[UTC]") :week))))

(testing "with 3 args (start/end inclusitivity options)"
(testing "exclusive start"
(is (= {:start (t/local-date-time "2019-10-31T23:59:59.999"), :end (t/local-date-time "2019-12-01T00:00")}
Expand Down
Loading

0 comments on commit 3cda9c8

Please sign in to comment.