├── util
├── .gitignore
├── README.md
├── package.json
├── yarn.lock
└── index.js
├── tests
├── .gitignore
├── elm-verify-examples.json
├── TestTimeZone.elm
├── elm-package.json
├── TestTimeZonePieces.elm
├── TestTimeZones.elm
├── TestDateTimeDelta.elm
├── TestZonedDateTime.elm
├── TestDate.elm
├── TestDateTime.elm
└── TestIso8601.elm
├── .gitignore
├── examples
├── with-timezone
│ ├── .gitignore
│ ├── README.md
│ ├── Makefile
│ ├── index.html
│ ├── elm-package.json
│ └── Main.elm
├── without-timezone
│ ├── .gitignore
│ ├── README.md
│ ├── Makefile
│ ├── index.html
│ ├── elm-package.json
│ └── Main.elm
└── with-parser-error-renderer
│ ├── .gitignore
│ ├── Makefile
│ ├── index.html
│ ├── elm-package.json
│ ├── README.md
│ └── Main.elm
├── .gitmodules
├── NOTES.md
├── elm-package.json
├── .travis.yml
├── src
└── Time
│ ├── Internal.elm
│ ├── Iso8601ErrorMsg.elm
│ ├── ZonedDateTime.elm
│ ├── Iso8601.elm
│ ├── TimeZone.elm
│ ├── Date.elm
│ └── DateTime.elm
├── LICENSE
└── README.md
/util/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
--------------------------------------------------------------------------------
/tests/.gitignore:
--------------------------------------------------------------------------------
1 | /elm-stuff/
2 | ./tests/VerifyExamples
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /elm-stuff
2 | /notes
3 | documentation.json
4 | /tmp
5 |
--------------------------------------------------------------------------------
/examples/with-timezone/.gitignore:
--------------------------------------------------------------------------------
1 | /elm-stuff
2 | /index.js
3 | /index.min.js
4 | npm-debug.log
5 |
--------------------------------------------------------------------------------
/examples/without-timezone/.gitignore:
--------------------------------------------------------------------------------
1 | /elm-stuff
2 | /index.js
3 | /index.min.js
4 | npm-debug.log
5 |
--------------------------------------------------------------------------------
/util/README.md:
--------------------------------------------------------------------------------
1 | # timezone-importer
2 |
3 | This module converts momentjs timezone data into `TimeZoneData.elm`.
4 |
--------------------------------------------------------------------------------
/examples/with-parser-error-renderer/.gitignore:
--------------------------------------------------------------------------------
1 | /elm-stuff
2 | /index.js
3 | /index.min.js
4 | npm-debug.log
5 | elm.js
6 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "elm-time.wiki"]
2 | path = elm-time.wiki
3 | url = https://github.com/elm-community/elm-time.wiki.git
4 |
--------------------------------------------------------------------------------
/examples/with-timezone/README.md:
--------------------------------------------------------------------------------
1 | # Show Example Web Page With Timezones
2 |
3 | See **[The Examples](https://github.com/elm-community/elm-time/wiki/The-Examples)**
4 |
--------------------------------------------------------------------------------
/examples/without-timezone/README.md:
--------------------------------------------------------------------------------
1 | # Show Example Web Page Without Timezones
2 |
3 | See **[The Examples](https://github.com/elm-community/elm-time/wiki/The-Examples)**
4 |
--------------------------------------------------------------------------------
/tests/elm-verify-examples.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": "../src",
3 | "tests": [
4 | "Time.Date",
5 | "Time.DateTime",
6 | "Time.Iso8601",
7 | "Time.Iso8601ErrorMsg"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/util/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "timezone-importer",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "license": "BSD3",
6 | "dependencies": {
7 | "moment-timezone": "^0.5.10"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/examples/with-timezone/Makefile:
--------------------------------------------------------------------------------
1 | SOURCES = $(shell find ../../src -type f -name "*.elm" -print)
2 |
3 | index.js: Main.elm $(SOURCES)
4 | elm make --output=index.js Main.elm
5 | uglifyjs -c dead_code,properties,conditionals,evaluate,unused index.js > index.min.js
6 |
--------------------------------------------------------------------------------
/examples/without-timezone/Makefile:
--------------------------------------------------------------------------------
1 | SOURCES = $(shell find ../../src -type f -name "*.elm" -print)
2 |
3 | index.js: Main.elm $(SOURCES)
4 | elm make --output=index.js Main.elm
5 | uglifyjs -c dead_code,properties,conditionals,evaluate,unused index.js > index.min.js
6 |
--------------------------------------------------------------------------------
/examples/with-parser-error-renderer/Makefile:
--------------------------------------------------------------------------------
1 | SOURCES = $(shell find ../../src -type f -name "*.elm" -print)
2 |
3 | index.js: Main.elm $(SOURCES)
4 | elm make --output=index.js Main.elm
5 | uglifyjs -c dead_code,properties,conditionals,evaluate,unused index.js > index.min.js
6 |
--------------------------------------------------------------------------------
/examples/with-parser-error-renderer/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | >
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/examples/with-timezone/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | elm-utc Example
5 |
6 |
7 |
8 |
9 |
10 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/examples/without-timezone/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | elm-utc Example
5 |
6 |
7 |
8 |
9 |
10 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/NOTES.md:
--------------------------------------------------------------------------------
1 | # Notes
2 |
3 | This is just a temporary internal document where I track what I'm doing.
4 |
5 | ## Needed TimeZone Tests
6 |
7 | | **Moment Name** | **Old Function Name** | **New Function Name** | **Implemented?** |
8 | |---|---|---|---|
9 | | unpack | TimeZone.unpack | unpack | Needs parsing |
10 | | unpackBase60 | base60 base60String
11 |
12 | ## Packed Timezone Format Change
13 |
14 | Between 2015 and 2016 the format was changed.
15 |
--------------------------------------------------------------------------------
/util/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 | moment-timezone:
4 | version "0.5.9"
5 | resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.9.tgz#e0ea82036d67d21d793544a91b5057f480eda2dd"
6 | dependencies:
7 | moment ">= 2.6.0"
8 |
9 | "moment@>= 2.6.0":
10 | version "2.16.0"
11 | resolved "https://registry.yarnpkg.com/moment/-/moment-2.16.0.tgz#f38f2c97c9889b0ee18fc6cc392e1e443ad2da8e"
12 |
13 |
--------------------------------------------------------------------------------
/examples/with-timezone/elm-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.0.1",
3 | "summary": "helpful summary of your project, less than 80 characters",
4 | "repository": "https://github.com/user/project.git",
5 | "license": "BSD3",
6 | "source-directories": [
7 | ".",
8 | "../../src"
9 | ],
10 | "exposed-modules": [],
11 | "dependencies": {
12 | "elm-community/json-extra": "2.0.0 <= v < 3.0.0",
13 | "elm-lang/core": "5.0.0 <= v < 6.0.0",
14 | "elm-lang/html": "2.0.0 <= v < 3.0.0",
15 | "elm-lang/lazy": "2.0.0 <= v < 3.0.0",
16 | "elm-tools/parser": "2.0.1 <= v < 3.0.0"
17 | },
18 | "elm-version": "0.18.0 <= v < 0.19.0"
19 | }
20 |
--------------------------------------------------------------------------------
/examples/without-timezone/elm-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.0.1",
3 | "summary": "helpful summary of your project, less than 80 characters",
4 | "repository": "https://github.com/user/project.git",
5 | "license": "BSD3",
6 | "source-directories": [
7 | ".",
8 | "../../src"
9 | ],
10 | "exposed-modules": [],
11 | "dependencies": {
12 | "elm-community/json-extra": "2.0.0 <= v < 3.0.0",
13 | "elm-lang/core": "5.0.0 <= v < 6.0.0",
14 | "elm-lang/html": "2.0.0 <= v < 3.0.0",
15 | "elm-lang/lazy": "2.0.0 <= v < 3.0.0",
16 | "elm-tools/parser": "2.0.1 <= v < 3.0.0"
17 | },
18 | "elm-version": "0.18.0 <= v < 0.19.0"
19 | }
20 |
--------------------------------------------------------------------------------
/examples/with-parser-error-renderer/elm-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.0.0",
3 | "summary": "elm-time example with rendering",
4 | "repository": "https://github.com/user/project.git",
5 | "license": "BSD3",
6 | "source-directories": [
7 | ".",
8 | "../../src"
9 | ],
10 | "exposed-modules": [],
11 | "dependencies": {
12 | "elm-lang/core": "5.1.1 <= v < 6.0.0",
13 | "elm-lang/html": "2.0.0 <= v < 3.0.0",
14 | "elm-lang/keyboard": "1.0.1 <= v < 2.0.0",
15 | "elm-lang/window": "1.0.1 <= v < 2.0.0",
16 | "elm-tools/parser": "2.0.1 <= v < 3.0.0",
17 | "mdgriffith/style-elements": "4.3.0 <= v < 5.0.0"
18 | },
19 | "elm-version": "0.18.0 <= v < 0.19.0"
20 | }
21 |
--------------------------------------------------------------------------------
/elm-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "3.0.5",
3 | "summary": "Deprecated: A pure Elm date and time library.",
4 | "repository": "https://github.com/elm-community/elm-time.git",
5 | "license": "BSD3",
6 | "source-directories": [
7 | "src"
8 | ],
9 | "exposed-modules": [
10 | "Time.Date",
11 | "Time.DateTime",
12 | "Time.Iso8601",
13 | "Time.Iso8601ErrorMsg",
14 | "Time.TimeZone",
15 | "Time.TimeZones",
16 | "Time.ZonedDateTime"
17 | ],
18 | "dependencies": {
19 | "elm-lang/core": "5.0.0 <= v < 6.0.0",
20 | "elm-lang/lazy": "2.0.0 <= v < 3.0.0",
21 | "elm-tools/parser": "2.0.1 <= v < 3.0.0"
22 | },
23 | "elm-version": "0.18.0 <= v < 0.19.0"
24 | }
25 |
--------------------------------------------------------------------------------
/tests/TestTimeZone.elm:
--------------------------------------------------------------------------------
1 | module TestTimeZone exposing (..)
2 |
3 | import Debug
4 | import Lazy exposing (force)
5 | import Expect exposing (Expectation)
6 | import Test exposing (..)
7 | import Time.TimeZones exposing (europe_bucharest)
8 | import Time.TimeZoneData exposing (unpack, africa_bissau_l)
9 | import Time.ZonedDateTime exposing (..)
10 |
11 |
12 | unpackTimeZoneData : Test
13 | unpackTimeZoneData =
14 | describe "Time.TimeZone.unpack"
15 | [ test "unpack converts a packed string into a TimeZone" <|
16 | \() ->
17 | let
18 | tz =
19 | force (unpack "Africa/Bissau|LMT -01 GMT|12.k 10 0|012|-2ldWV.E 2xonV.E|39e4")
20 | in
21 | Expect.equal (force africa_bissau_l) tz
22 | ]
23 |
--------------------------------------------------------------------------------
/tests/elm-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.0.0",
3 | "summary": "Test Suites",
4 | "repository": "https://github.com/elm-community/elm-time.git",
5 | "license": "BSD3",
6 | "source-directories": [
7 | "../src",
8 | "."
9 | ],
10 | "exposed-modules": [
11 | "Time.Date",
12 | "Time.DateTime",
13 | "Time.Iso8601",
14 | "Time.ISO8601Error",
15 | "Time.TimeZone",
16 | "Time.TimeZones",
17 | "Time.ZonedDateTime"
18 | ],
19 | "dependencies": {
20 | "elm-community/elm-test": "4.0.0 <= v < 5.0.0",
21 | "elm-community/json-extra": "2.0.0 <= v < 3.0.0",
22 | "elm-lang/core": "5.0.0 <= v < 6.0.0",
23 | "elm-lang/lazy": "2.0.0 <= v < 3.0.0",
24 | "elm-tools/parser": "2.0.1 <= v < 3.0.0"
25 | },
26 | "elm-version": "0.18.0 <= v < 0.19.0"
27 | }
28 |
--------------------------------------------------------------------------------
/tests/TestTimeZonePieces.elm:
--------------------------------------------------------------------------------
1 | module TestTimeZonePieces exposing (..)
2 |
3 | --import Lazy exposing (force)
4 |
5 | import Debug
6 | import Expect exposing (Expectation, equal, fail)
7 | import Test exposing (..)
8 | import Time.TimeZone exposing (TimeZone)
9 | import Parser exposing (run)
10 |
11 |
12 | source =
13 | "Africa/Bissau|LMT -01 GMT|12.k 10 0|012|-2ldWV.E 2xonV.E|39e4"
14 |
15 |
16 | unpackTest : Test
17 | unpackTest =
18 | describe "Time.TimeZone.unpack"
19 | [ test "New" <|
20 | \() ->
21 | case Time.TimeZone.unpack source of
22 | Ok value ->
23 | let
24 | v =
25 | value
26 | in
27 | Expect.pass
28 |
29 | Err msg ->
30 | fail (toString msg)
31 | ]
32 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js: node
3 |
4 | cache:
5 | directories:
6 | - elm-stuff/build-artifacts
7 | - tests/elm-stuff/build-artifacts
8 | - sysconfcpus
9 |
10 | before_install:
11 | - | # epic build time improvement - see https://github.com/elm-lang/elm-compiler/issues/1473#issuecomment-245704142
12 | if [ ! -d sysconfcpus/bin ];
13 | then
14 | git clone https://github.com/obmarg/libsysconfcpus.git;
15 | cd libsysconfcpus;
16 | ./configure --prefix=$TRAVIS_BUILD_DIR/sysconfcpus;
17 | make && make install;
18 | cd ..;
19 | fi
20 |
21 | install:
22 | - npm install -g elm@0.18.0 elm-test@0.18.12 elm-verify-examples@2.3.1 elm-format@0.6.1-alpha
23 | - mv $(npm config get prefix)/bin/elm-make $(npm config get prefix)/bin/elm-make-old
24 | - printf '%s\n\n' '#!/bin/bash' 'echo "Running elm-make with sysconfcpus -n 2"' '$TRAVIS_BUILD_DIR/sysconfcpus/bin/sysconfcpus -n 2 elm-make-old "$@"' > $(npm config get prefix)/bin/elm-make
25 | - chmod +x $(npm config get prefix)/bin/elm-make
26 | - elm-package install --yes
27 |
28 | script:
29 | - elm-format --validate src tests examples
30 | - elm-test
31 | - elm-verify-examples
32 |
--------------------------------------------------------------------------------
/tests/TestTimeZones.elm:
--------------------------------------------------------------------------------
1 | module TestTimeZones exposing (..)
2 |
3 | import Dict
4 | import Expect exposing (Expectation)
5 | import List
6 | import Test exposing (..)
7 | import Time.TimeZone
8 | import Time.TimeZones
9 |
10 |
11 | linkedTimeZoneTest : Test
12 | linkedTimeZoneTest =
13 | describe "Time.TimeZones"
14 | [ test "Europe/Zagreb is an alias for Europe/Belgrade" <|
15 | \() ->
16 | Time.TimeZones.fromName "Europe/Zagreb"
17 | -- Rename the zone data returned so we can compare just
18 | -- the `spans`, i.e. the actual time zone data.
19 | |> Maybe.map (Time.TimeZone.setName "Europe/Belgrade")
20 | |> Expect.equal (Just (Time.TimeZones.europe_belgrade ()))
21 | ]
22 |
23 |
24 | namedTimeZoneTest : Test
25 | namedTimeZoneTest =
26 | describe "Time.TimeZones naming" <|
27 | let
28 | check ( timeZoneName, loadTimeZone ) =
29 | test (timeZoneName ++ "'s TimeZone is named " ++ timeZoneName) <|
30 | \() -> loadTimeZone () |> Time.TimeZone.name |> Expect.equal timeZoneName
31 | in
32 | Dict.toList Time.TimeZones.all |> List.map check
33 |
--------------------------------------------------------------------------------
/examples/without-timezone/Main.elm:
--------------------------------------------------------------------------------
1 | module Main exposing (..)
2 |
3 | import Html as H exposing (Html)
4 | import Time exposing (Time)
5 | import Time.DateTime as DateTime exposing (DateTime)
6 |
7 |
8 | type alias Flags =
9 | { now : Time }
10 |
11 |
12 | type alias Model =
13 | { now : DateTime
14 | }
15 |
16 |
17 | type Msg
18 | = Tick Time
19 |
20 |
21 | main : Program Flags Model Msg
22 | main =
23 | H.programWithFlags
24 | { init = init
25 | , update = update
26 | , view = view
27 | , subscriptions = subscriptions
28 | }
29 |
30 |
31 | init : Flags -> ( Model, Cmd Msg )
32 | init { now } =
33 | { now = DateTime.fromTimestamp now
34 | }
35 | ! []
36 |
37 |
38 | update : Msg -> Model -> ( Model, Cmd Msg )
39 | update msg model =
40 | case msg of
41 | Tick now ->
42 | ( { model | now = DateTime.fromTimestamp now }
43 | , Cmd.none
44 | )
45 |
46 |
47 | view : Model -> Html Msg
48 | view { now } =
49 | H.div []
50 | [ DateTime.toISO8601 now
51 | |> H.text
52 | ]
53 |
54 |
55 | subscriptions : Model -> Sub Msg
56 | subscriptions model =
57 | Sub.batch [ Time.every Time.second Tick ]
58 |
--------------------------------------------------------------------------------
/src/Time/Internal.elm:
--------------------------------------------------------------------------------
1 | module Time.Internal exposing (..)
2 |
3 |
4 | type alias DateTimeData =
5 | { year : Int
6 | , month : Int
7 | , day : Int
8 | , hour : Int
9 | , minute : Int
10 | , second : Int
11 | , millisecond : Int
12 | }
13 |
14 |
15 | type alias TimeData d =
16 | { d
17 | | hour : Int
18 | , minute : Int
19 | , second : Int
20 | , millisecond : Int
21 | }
22 |
23 |
24 | offsetFromTimeData : TimeData d -> Int
25 | offsetFromTimeData { hour, minute, second, millisecond } =
26 | clamp 0 23 hour * hourMs + clamp 0 59 minute * minuteMs + clamp 0 59 second * secondMs + clamp 0 999 millisecond
27 |
28 |
29 | zero : DateTimeData
30 | zero =
31 | { year = 0
32 | , month = 1
33 | , day = 1
34 | , hour = 0
35 | , minute = 0
36 | , second = 0
37 | , millisecond = 0
38 | }
39 |
40 |
41 | padded : Int -> String
42 | padded n =
43 | if n < 10 then
44 | "0" ++ toString n
45 | else
46 | toString n
47 |
48 |
49 | padded3 : Int -> String
50 | padded3 n =
51 | String.padLeft 3 '0' (toString n)
52 |
53 |
54 | dayMs : number
55 | dayMs =
56 | 86400000
57 |
58 |
59 | hourMs : number
60 | hourMs =
61 | 3600000
62 |
63 |
64 | minuteMs : number
65 | minuteMs =
66 | 60000
67 |
68 |
69 | secondMs : number
70 | secondMs =
71 | 1000
72 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016, Bogdan Paul Popa
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 | * Redistributions of source code must retain the above copyright
7 | notice, this list of conditions and the following disclaimer.
8 | * Redistributions in binary form must reproduce the above copyright
9 | notice, this list of conditions and the following disclaimer in the
10 | documentation and/or other materials provided with the distribution.
11 | * Neither the name of the nor the
12 | names of its contributors may be used to endorse or promote products
13 | derived from this software without specific prior written permission.
14 |
15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY
19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/tests/TestDateTimeDelta.elm:
--------------------------------------------------------------------------------
1 | module TestDateTimeDelta exposing (..)
2 |
3 | import Time.DateTime as DateTime exposing (..)
4 | import Expect exposing (Expectation)
5 |
6 |
7 | -- import Fuzz exposing (Fuzzer, constant, int, intRange, oneOf)
8 |
9 | import Test exposing (..)
10 |
11 |
12 | date1 : DateTime
13 |
14 |
15 |
16 | -- hmm
17 |
18 |
19 | date1 =
20 | DateTime.fromTuple
21 | ( 1970
22 | , 1
23 | , 1
24 | , 0
25 | , 0
26 | , 0
27 | , 0
28 | )
29 |
30 |
31 | date2 : DateTime
32 | date2 =
33 | DateTime.fromTuple
34 | ( 1969
35 | , 12
36 | , 31
37 | , 23
38 | , 59
39 | , 59
40 | , 999
41 | )
42 |
43 |
44 | expectedDelta : DateTimeDelta
45 | expectedDelta =
46 | { years = 1 -- force one if across a year boundary (1969 -> 1970)
47 | , months = 1 -- force one if across a month boundary (Dec -> Jan)
48 | , days = 1 -- force one if across a day boundary (31 -> 1)
49 | , hours = 0 -- round (ignore hour boundary)
50 | , minutes = 0 -- round (ignore minute boundary)
51 | , seconds = 0 -- round (ignore second boundary)
52 | , milliseconds = 1 -- whatever it is
53 | }
54 |
55 |
56 | suite : Test
57 | suite =
58 | describe "DateTime delta"
59 | [ test "Exploratory: two dates one millisecond apart at midnight New Years" <|
60 | \_ ->
61 | DateTime.delta date1 date2
62 | -- |> Debug.log "date2 - date1"
63 | |> Expect.equal expectedDelta
64 | ]
65 |
--------------------------------------------------------------------------------
/examples/with-parser-error-renderer/README.md:
--------------------------------------------------------------------------------
1 | # Example Page Demonstrating Error Rendering
2 |
3 | In this demonstration, you modify an **ISO8601** string in a text input
4 | and see the parsed result. If the string has no parsing or validation
5 | errors, you see a grid of the parsed result. Otherwise, an "Elm-style"
6 | error message is shown, followed by the **parser**'s error structure
7 | that the error renderer needed in order to interpret to generate
8 | the error message.
9 |
10 | ## Installation
11 |
12 | ### Prerequisites
13 |
14 | | **Name** | **Version** | **Installation** |
15 | |---|---|---|
16 | | elm | \>= 0.18.0 | **[Install](https://guide.elm-lang.org/install.html)** |
17 | | node | v8.9.3 | **[node dist](https://nodejs.org/dist/v8.9.3/)** |
18 | | npm | 5.8.0 | Included as part of **node** install above |
19 | | elm-live | \>= v2.7.5 | `npm install -g elm-live` |
20 |
21 | ### Install and Run
22 |
23 | For this you need to clone `elm-community/elm-time` from GitHub
24 | into your directory of other Elm projects as follows:
25 |
26 | 1. `cd` into your Elm projects' folder into which you want to clone
27 | `elm-time`.
28 |
29 | 1. Perform the following commands:
30 | ```bash
31 | git clone git@github.com:elm-community/elm-time.git
32 | cd elm-time
33 | elm-make --warn
34 | cd examples/with-parser-error-renderer
35 | elm-live --output=elm.js Main.elm --open --warn
36 | ```
37 | 1. Browse http://localhost:8000 and observe that the initial **ISO8601**
38 | string is parsed successfully into a row of labeled boxes.
39 | 1. In the input text edit control with the light-gray background,
40 | change the **ISO8601** string to make the parsing fail.
41 | 1. Hit the `Enter` key and observe the error message and parsing
42 | error structure in red.
43 |
44 | Try different **ISO8601** string values and see what this page does
45 | with them.
46 |
--------------------------------------------------------------------------------
/tests/TestZonedDateTime.elm:
--------------------------------------------------------------------------------
1 | module TestZonedDateTime exposing (..)
2 |
3 | import Expect exposing (Expectation)
4 | import Test exposing (..)
5 | import Time.DateTime as DT
6 | import Time.TimeZones exposing (europe_bucharest)
7 | import Time.ZonedDateTime exposing (..)
8 |
9 |
10 | convertingToDateTime : Test
11 | convertingToDateTime =
12 | describe "Time.DateTime.toDateTime"
13 | [ test "DateTime to Zoned and back produces correct result" <|
14 | \() ->
15 | let
16 | dt =
17 | DT.dateTime { zero | year = 2016, month = 10, day = 30, hour = 1, minute = 30 }
18 | in
19 | dt
20 | |> fromDateTime (europe_bucharest ())
21 | |> toDateTime
22 | |> Expect.equal dt
23 | , test "DateTime from naive Zoned produces correct result" <|
24 | \() ->
25 | zonedDateTime (europe_bucharest ()) { zero | year = 2016, month = 10, day = 30, hour = 1, minute = 30 }
26 | |> toDateTime
27 | |> Expect.equal (DT.dateTime { zero | year = 2016, month = 10, day = 29, hour = 22, minute = 30 })
28 | , test "DateTime from naive Zoned produces correct result during the end of summer time" <|
29 | \() ->
30 | zonedDateTime (europe_bucharest ()) { zero | year = 2016, month = 10, day = 30, hour = 3 }
31 | |> toDateTime
32 | |> Expect.equal (DT.dateTime { zero | year = 2016, month = 10, day = 30, hour = 1 })
33 | , test "DateTime from naive Zoned produces correct result after the end of summer time" <|
34 | \() ->
35 | zonedDateTime (europe_bucharest ()) { zero | year = 2016, month = 10, day = 30, hour = 4 }
36 | |> toDateTime
37 | |> Expect.equal (DT.dateTime { zero | year = 2016, month = 10, day = 30, hour = 2 })
38 | ]
39 |
--------------------------------------------------------------------------------
/examples/with-timezone/Main.elm:
--------------------------------------------------------------------------------
1 | module Main exposing (..)
2 |
3 | import Html as H exposing (Html)
4 | import Html.Attributes as A
5 | import Html.Events as E
6 | import Json.Decode as JD
7 | import Time exposing (Time)
8 | import Time.DateTime as DT exposing (DateTime)
9 | import Time.TimeZone exposing (TimeZone)
10 | import Time.TimeZones as TimeZones
11 | import Time.Iso8601
12 | import Time.ZonedDateTime
13 |
14 |
15 | type alias Flags =
16 | { now : Time }
17 |
18 |
19 | type alias Model =
20 | { now : DateTime
21 | , zone : TimeZone
22 | }
23 |
24 |
25 | type Msg
26 | = Tick Time
27 | | ChangeZone String
28 |
29 |
30 | main : Program Flags Model Msg
31 | main =
32 | H.programWithFlags
33 | { init = init
34 | , update = update
35 | , view = view
36 | , subscriptions = subscriptions
37 | }
38 |
39 |
40 | init : Flags -> ( Model, Cmd Msg )
41 | init { now } =
42 | { now = DT.fromTimestamp now
43 | , zone = TimeZones.utc ()
44 | }
45 | ! []
46 |
47 |
48 | update : Msg -> Model -> ( Model, Cmd Msg )
49 | update msg model =
50 | case msg of
51 | Tick now ->
52 | { model | now = DT.fromTimestamp now } ! []
53 |
54 | ChangeZone zoneString ->
55 | let
56 | zone =
57 | case zoneString of
58 | "Europe/Bucharest" ->
59 | TimeZones.europe_bucharest ()
60 |
61 | "US/Central" ->
62 | TimeZones.us_central ()
63 |
64 | _ ->
65 | TimeZones.utc ()
66 | in
67 | ( { model | zone = zone }
68 | , Cmd.none
69 | )
70 |
71 |
72 | view : Model -> Html Msg
73 | view { now, zone } =
74 | H.div []
75 | [ H.select [ E.on "change" (JD.map ChangeZone E.targetValue) ]
76 | [ H.option [ A.value "UTC" ] [ H.text "UTC" ]
77 | , H.option [ A.value "Europe/Bucharest" ] [ H.text "Europe/Bucharest" ]
78 | , H.option [ A.value "US/Central" ] [ H.text "US/Central" ]
79 | ]
80 | , H.br [] []
81 | , Time.ZonedDateTime.fromDateTime zone now
82 | |> Time.Iso8601.fromZonedDateTime
83 | |> H.text
84 | ]
85 |
86 |
87 | subscriptions : Model -> Sub Msg
88 | subscriptions model =
89 | Sub.batch [ Time.every Time.second Tick ]
90 |
--------------------------------------------------------------------------------
/util/index.js:
--------------------------------------------------------------------------------
1 | let fs = require("fs");
2 | let moment = require("moment-timezone");
3 |
4 | // `names` contains a mapping from "flat" zone names to their
5 | // corresponding IANA/Olsen name, e.g. europe_london -> 'Europe/London'.
6 | // There are entries for canonical zone names as well as aliases and
7 | // deprecated names.
8 | let names = moment.tz._names;
9 | // `links` contains a mapping from flat zone aliases and deprecated
10 | // names to their canonical sanitized name, e.g. europe_belfast ->
11 | // 'europe_london'. HOWEVER, there are also mappings from SOME canonical
12 | // sanitized names to other names, like europe_london -> 'gb-eire'. It's
13 | // not clear why these latter entries exist.
14 | let links = moment.tz._links;
15 | // `zones` contains a mapping from flat zone names to actual zone data,
16 | // e.g. europe_london -> 'Europe/London|GMT BST BDST|0 -1 ...'. It
17 | // appears to only contain mappings for canonical zone names.
18 | let zones = moment.tz._zones;
19 |
20 | let sanitize = (name) => {
21 | return name.
22 | toLowerCase().
23 | replace(/gmt\+/, "gmt_plus_").
24 | replace(/gmt\-/, "gmt_minus_").
25 | replace(/gmt0/, "gmt_0").
26 | replace(/-/g, "_");
27 | };
28 |
29 | // Data file
30 | let timeZones = [], timeZoneData = [];
31 |
32 | // Date file: Populate canonical zone data.
33 | for (let name in zones) {
34 | let zone = zones[name];
35 | let link = links[name];
36 | let fullName = names[name];
37 |
38 | name = sanitize(name);
39 | timeZoneData.push(`${name}_l = unpack "${zone}"`);
40 | timeZones.push({name, fullName, link: false});
41 | }
42 |
43 | // Data file: Populate aliases.
44 | for (let name in links) {
45 | let link = links[name];
46 | // Only add an alias when it refers to a canonical zone.
47 | if (zones[link]) {
48 | timeZones.push({
49 | name: sanitize(name),
50 | fullName: names[name],
51 | timeZone: sanitize(link),
52 | link: true
53 | });
54 | }
55 | }
56 |
57 | timeZones.sort((a, b) => {
58 | return a.name > b.name ? 1 : -1;
59 | });
60 | timeZoneData.sort();
61 |
62 | // TimeZones file
63 | let docs = [], fns = [], all = [];
64 | for (let i = 0; i < timeZones.length; i++) {
65 | let timeZone = timeZones[i];
66 |
67 | docs.push(timeZone.name);
68 | all.push(`("${timeZone.fullName}", ${timeZone.name})`);
69 |
70 | if (timeZone.link) {
71 | fns.push(`
72 | {-| ${timeZone.fullName} -}
73 | ${timeZone.name} : () -> TimeZone
74 | ${timeZone.name} () = force (link "${timeZone.fullName}" ${timeZone.timeZone}_l)`);
75 | } else {
76 | fns.push(`
77 | {-| ${timeZone.fullName} -}
78 | ${timeZone.name} : () -> TimeZone
79 | ${timeZone.name} () = force ${timeZone.name}_l`);
80 | }
81 | }
82 |
83 | fs.open("../src/Time/TimeZoneData.elm", "w", (err, fd) => {
84 | let content = `
85 | module Time.TimeZoneData exposing (..)
86 |
87 | import Lazy exposing (Lazy, force, lazy)
88 | import String
89 | import Time.TimeZone exposing (TimeZone, setName)
90 |
91 |
92 | unpack : String -> Lazy TimeZone
93 | unpack data =
94 | let
95 | helper () =
96 | case Time.TimeZone.unpack data of
97 | Err errors ->
98 | let
99 | messages =
100 | String.join " or " errors
101 | in
102 | Debug.crash ("failed to parse zone '" ++ data ++ "': " ++ messages)
103 |
104 | Ok zone ->
105 | zone
106 | in
107 | lazy helper
108 |
109 |
110 | link : String -> Lazy TimeZone -> Lazy TimeZone
111 | link link lz =
112 | Lazy.map (setName link) lz
113 |
114 |
115 | -- Data
116 | -- ----
117 | ${timeZoneData.join("\n")}`.trim();
118 |
119 | fs.write(fd, content, (err) => {
120 | if (err) throw new err;
121 | });
122 | });
123 |
124 | // Chunk `all` to prevent stack overflow in Elm 0.18 with large list literals.
125 | let all_chunks = [], all_chunk_size = 50;
126 | for (let i = 0; i < all.length; i += all_chunk_size) {
127 | let slice = all.slice(i, i + all_chunk_size);
128 | all_chunks.push("[ " + slice.join("\n , ") + "\n ]");
129 | }
130 |
131 | fs.open("../src/Time/TimeZones.elm", "w", (err, fd) => {
132 | let content = `
133 | module Time.TimeZones exposing (..)
134 |
135 | {-| This module contains TimeZone definitions for all Timezones as they
136 | are defined in the IANA zoneinfo database.
137 |
138 | TimeZone data is parsed lazily so, in order to retrieve a zone's value you
139 | must apply \`()\` to it. For example:
140 |
141 | import Time.DateTime exposing (epoch, toTimestamp)
142 | import Time.TimeZone exposing (abbreviation)
143 | import Time.TimeZones exposing (europe_bucharest)
144 |
145 | let
146 | timezone = europe_bucharest ()
147 | in
148 | abbreviation (toTimestamp epoch) timezone
149 |
150 | @docs all, fromName, ${docs.join(", ")}
151 | -}
152 |
153 | import Dict exposing (Dict)
154 | import Lazy exposing (Lazy, force)
155 | import Time.TimeZone exposing (TimeZone)
156 | import Time.TimeZoneData exposing (..)
157 |
158 |
159 | -- TimeZones
160 | -- ---------
161 | ${fns.join("\n")}
162 |
163 |
164 | -- Utils
165 | -- -----
166 | {-| A mapping from TimeZone names to their respective functions. Use
167 | this to look up TimeZones by name. -}
168 | all : Dict String (() -> TimeZone)
169 | all =
170 | Dict.fromList <|
171 | List.concat
172 | [ ${all_chunks.join("\n , ")}
173 | ]
174 |
175 |
176 | {-| Look up a TimeZone by name. -}
177 | fromName : String -> Maybe TimeZone
178 | fromName name =
179 | case Dict.get name all of
180 | Nothing ->
181 | Nothing
182 |
183 | Just f ->
184 | Just (f ())
185 | `.trim();
186 |
187 | fs.write(fd, content, (err) => {
188 | if (err) throw new err;
189 | });
190 | });
191 |
--------------------------------------------------------------------------------
/examples/with-parser-error-renderer/Main.elm:
--------------------------------------------------------------------------------
1 | module Main exposing (main)
2 |
3 | {-| An Elm web client application to exercise
4 | the `elm-time` library.
5 |
6 |
7 | # Main entry
8 |
9 | @docs main
10 |
11 | -}
12 |
13 | import Color exposing (..)
14 | import Element exposing (..)
15 | import Element.Attributes exposing (..)
16 | import Element.Input as Input
17 | import Html exposing (Html)
18 | import Keyboard exposing (KeyCode)
19 | import Parser exposing (Error)
20 | import Style exposing (..)
21 | import Style.Border as Border
22 | import Style.Color
23 | import Style.Font as Font
24 | import Task
25 | import Time.DateTime
26 | exposing
27 | ( DateTime
28 | , dateTime
29 | , fromTimestamp
30 | , fromTuple
31 | , year
32 | , month
33 | , day
34 | , hour
35 | , minute
36 | , second
37 | , millisecond
38 | )
39 | import Time.Iso8601ErrorMsg exposing (reflow, renderText)
40 | import Time.Iso8601 exposing (toDateTime)
41 | import Window
42 |
43 |
44 | type Msg
45 | = ChangeText String
46 | | Resize Window.Size
47 | | KeyDown Keyboard.KeyCode
48 |
49 |
50 | type Styles
51 | = None
52 | | InputContainer
53 | | Page
54 | | DateTimeGrid
55 | | DateTimeRow
56 | | Box
57 | | Error
58 |
59 |
60 | stylesheet : StyleSheet Styles variation
61 | stylesheet =
62 | Style.styleSheet
63 | [ style Page
64 | []
65 | , style InputContainer
66 | [ Style.Color.text black
67 | , Style.Color.background lightGray
68 | , Border.all 2
69 | ]
70 | , style Error
71 | [ Style.Color.text red
72 | , Font.typeface [ Font.monospace ]
73 | ]
74 | , style DateTimeGrid
75 | [ Border.top 2
76 | , Border.bottom 2
77 | , Border.left 1
78 | , Border.right 1
79 | ]
80 | , style Box
81 | [ Border.left 1
82 | , Border.right 1
83 | , Font.center
84 | ]
85 | ]
86 |
87 |
88 | {-| The "entry"
89 | -}
90 | main : Program Never Model Msg
91 | main =
92 | Html.program
93 | { init = init
94 | , update = update
95 | , view = view
96 | , subscriptions = subscriptions
97 | }
98 |
99 |
100 | type alias Model =
101 | { iso8601input : String
102 | , dateTime : Result Error DateTime
103 | , device : Device
104 | }
105 |
106 |
107 | init : ( Model, Cmd Msg )
108 | init =
109 | let
110 | initInput =
111 | "1991-02-28T12:25:12.0Z"
112 |
113 | initDateTime =
114 | dateTime
115 | { year = 1991
116 | , month = 2
117 | , day = 28
118 | , hour = 12
119 | , minute = 25
120 | , second = 12
121 | , millisecond = 0
122 | }
123 | in
124 | ( { iso8601input = initInput
125 | , dateTime = Ok initDateTime
126 | , device = classifyDevice (Window.Size 0 0)
127 | }
128 | , Task.perform Resize Window.size
129 | )
130 |
131 |
132 | update : Msg -> Model -> ( Model, Cmd Msg )
133 | update msg model =
134 | case msg of
135 | ChangeText text ->
136 | ( { model
137 | | iso8601input = text
138 | }
139 | , Cmd.none
140 | )
141 |
142 | Resize size ->
143 | ( { model
144 | | device = classifyDevice size
145 | }
146 | , Cmd.none
147 | )
148 |
149 | KeyDown keyCode ->
150 | if enterKeyCode == keyCode then
151 | runParse model
152 | else
153 | ( model, Cmd.none )
154 |
155 |
156 | view : Model -> Html Msg
157 | view model =
158 | Element.layout stylesheet <|
159 | el None [ center, width (px 800) ] <|
160 | column Page
161 | [ spacing 20 ]
162 | [ Input.text InputContainer
163 | [ padding 10 ]
164 | { onChange = ChangeText
165 | , value = model.iso8601input
166 | , label =
167 | Input.placeholder
168 | { label = Input.labelLeft (el None [ verticalCenter ] (text "Change ISO8601 string, then hit Enter:"))
169 | , text = "Test ISO8601 here"
170 | }
171 | , options = []
172 | }
173 | , case model.dateTime of
174 | Ok v ->
175 | renderDateTimeSuccess v
176 |
177 | Err err ->
178 | renderDateTimeFail err
179 | ]
180 |
181 |
182 | subscriptions : Model -> Sub Msg
183 | subscriptions model =
184 | Sub.batch
185 | [ Window.resizes Resize
186 | , Keyboard.downs KeyDown
187 | ]
188 |
189 |
190 | runParse : Model -> ( Model, Cmd Msg )
191 | runParse model =
192 | case toDateTime model.iso8601input of
193 | Ok v ->
194 | ( { model
195 | | dateTime = Ok v
196 | }
197 | , Cmd.none
198 | )
199 |
200 | Err msg ->
201 | ( { model
202 | | dateTime = Err msg
203 | }
204 | , Cmd.none
205 | )
206 |
207 |
208 | renderDateTimeSuccess : DateTime -> Element Styles a b
209 | renderDateTimeSuccess dt =
210 | let
211 | renderCell x y value =
212 | cell
213 | { start = ( x, y )
214 | , width = 1
215 | , height = 1
216 | , content = el Box [] (text <| value)
217 | }
218 | in
219 | grid DateTimeGrid
220 | []
221 | { columns = [ px 100, px 100, px 100 ]
222 | , rows = [ px 20, px 20 ]
223 | , cells =
224 | [ renderCell 0 0 "Year"
225 | , renderCell 1 0 "Month"
226 | , renderCell 2 0 "Day"
227 | , renderCell 3 0 "Hour"
228 | , renderCell 4 0 "Minute"
229 | , renderCell 5 0 "Second"
230 | , renderCell 6 0 "Millisecond"
231 | , renderCell 0 1 (toString <| year dt)
232 | , renderCell 1 1 (toString <| month dt)
233 | , renderCell 2 1 (toString <| day dt)
234 | , renderCell 3 1 (toString <| hour dt)
235 | , renderCell 4 1 (toString <| minute dt)
236 | , renderCell 5 1 (toString <| second dt)
237 | , renderCell 6 1 (toString <| millisecond dt)
238 | ]
239 | }
240 |
241 |
242 |
243 | -- el Success [] (text <| toString dateTime)
244 |
245 |
246 | renderDateTimeFail : Error -> Element Styles a b
247 | renderDateTimeFail err =
248 | el Error [] (text <| renderText err ++ "\n\n---\n\n" ++ reflow "The Elm error returned by parser:\n\n" ++ reflow (toString err))
249 |
250 |
251 | enterKeyCode : KeyCode
252 | enterKeyCode =
253 | 13
254 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # elm-time [](https://travis-ci.org/elm-community/elm-time)
2 |
3 | ``` shell
4 | elm package install elm-community/elm-time
5 | ```
6 |
7 | ## This Package is Released to Elm 0.19 -- Also Archived!
8 |
9 | Isaac Seymour has ported
10 | **[elm-community/elm-time](https://package.elm-lang.org/packages/elm-community/elm-time/3.0.4/)** to
11 | **[isaacseymour/deprecated-time](https://package.elm-lang.org/packages/isaacseymour/deprecated-time/latest)**.
12 | With the change in **elm/time** to use Posix, and a consensus to break this package up so that you don't have to
13 | "include everything" to use it, we decided that this was a good time to deprecate. *On October 6, 2021, this package was archived.*
14 |
15 | That being said, we will continue to support `isaacseymour/deprecated-time` with bug fixes for the foreseeable future.
16 |
17 | Thanks to Isaac for performing this upgrade!
18 |
19 | ## Major Changes!
20 |
21 | This release prepares **elm-time** to be upgraded to Elm `0.19` by changing
22 | out the **ISO8601** and **Timezone Name** parsing from the
23 | **[parser-combinators](http://package.elm-lang.org/packages/elm-community/parser-combinators/latest)** parser to
24 | Evan's **[parser](http://package.elm-lang.org/packages/elm-tools/parser/latest)**.
25 |
26 | > NOTE: this release is probably the last Elm `0.18` release.
27 |
28 | Hence, the changes are extensive and some API's have changed. Here's a summary
29 | of them:
30 |
31 | * **ISO8601** processing has been broken out into its own module: `Time.Iso8601`.
32 | * An "Elm-style" error renderer for **ISO8601** parsing errors is provided: `Time.Iso8601ErrorMsg`.
33 | * An example Elm client-application showing the error handling is provided in `/examples/with-parser-error-renderer`.
34 | * Each of the public APIs in `Time.Date`, `Time.DateTime`, `Time.ZonedDateTime`, `Iso8601`, and `Iso8601ErrorMsg`
35 | now has extensive
36 | **["verify examples"](https://github.com/stoeffel/elm-verify-examples)** documentation.
37 |
38 | ## Examples
39 |
40 | * **[Error Rendering Example](https://github.com/elm-community/elm-time/blob/daa0e1b60a912519af5b699cc26c0b17a6e06257/examples/with-parser-error-renderer/README.md)**
41 | * **[Running and Understanding Timezone Examples](https://github.com/elm-community/elm-time/wiki/The-Examples)**
42 |
43 | ## Dates
44 |
45 | `Date`s may represent any date in the [proleptic Gregorian calendar][cal].
46 |
47 | ``` elm
48 | import Time.Date as Date exposing (Date, date)
49 | ```
50 |
51 | ### Constructing Dates
52 |
53 | Use `date` to construct `Date` values. If given invalid values for
54 | the month and day, they are both clamped and the nearest valid date is
55 | returned.
56 |
57 | ``` elm
58 | > date 1992 2 28
59 | Date { year = 1992, month = 2, day = 28 } : Date
60 |
61 | > date 1992 2 31
62 | Date { year = 1992, month = 2, day = 29 } : Date
63 |
64 | > date 1992 2 128
65 | Date { year = 1992, month = 2, day = 29 } : Date
66 | ```
67 |
68 | Use `year`, `month`, and `day` to inspect `Date`s.
69 |
70 | ``` elm
71 | > d = date 1992 5 29
72 | Date { year = 1992, month = 5, day = 29 } : Date
73 |
74 | > Date.year d
75 | 1992 : Int
76 |
77 | > Date.month d
78 | 5 : Int
79 |
80 | > Date.day d
81 | 29 : Int
82 | ```
83 |
84 | ### Manipulating Dates
85 |
86 | `setYear`, `setMonth` and `setDay` can be used to create new `Dates`
87 | containing updated values for each respective field. Like `date`,
88 | these functions clamp their parameters and return the nearest valid
89 | date.
90 |
91 | `addDays` can be used to add an exact number of days to a `Date`.
92 |
93 | `addYears` and `addMonths` add a relative number of years and months
94 | to a date. If the target date is invalid, these functions continually
95 | subtract one day until a valid date is found.
96 |
97 | ``` elm
98 | import Time.Date as Date exposing (Date, date, addYears)
99 | import Time.Iso8601
100 |
101 | > date 1992 1 31
102 | | |> addYears 1
103 | | |> Time.Iso8601.fromDate
104 | "1993-01-31" : String
105 |
106 | > date 1992 2 29
107 | | |> addYears 1
108 | | |> Time.Iso8601.fromDate
109 | "1993-02-28" : String
110 |
111 | > date 1992 1 31
112 | | |> Date.addMonths 1
113 | | |> Time.Iso8601.fromDate
114 | "1992-02-28" : String
115 | ```
116 |
117 | ## DateTimes
118 |
119 | `DateTimes` represent a `Date` together with the time information starting on midnight for the `Date`.
120 |
121 | ``` elm
122 | import Time.DateTime as DateTime exposing (DateTime, dateTime, year, month, day, hour, minute, second, millisecond)
123 | ```
124 |
125 | ### Constructing DateTimes
126 |
127 | `DateTime`s can be constructed from a record using the `dateTime`
128 | function or from a UTC timestamp in milliseconds using `fromTimestamp`.
129 | To construct a `DateTime` using `dateTime`, pass it a record
130 | containing fields for `year`, `month`, `day`, `hour`, `minute`,
131 | `second` and `millisecond`:
132 |
133 | ``` elm
134 | dt : DateTime
135 | dt =
136 | dateTime { year = 1992, month = 5, day = 29, hour = 0, minute = 0, second = 0, millisecond = 0 }
137 |
138 | year dt --> 1992
139 | month dt --> 5
140 | day dt --> 29
141 | hour dt --> 0
142 | minute dt --> 0
143 | second --> 0
144 | millisecond --> 0
145 |
146 | dt : DateTime
147 | dt =
148 | dateTime { year = 1992, month = 2, day = 31, hour = 0, minute = 0, second = 0, millisecond = 0 }
149 |
150 | year dt --> 1992
151 | month dt --> 2
152 | day dt --> 29 - Note clamped.
153 | hour dt --> 0
154 | minute dt --> 0
155 | second --> 0
156 | millisecond --> 0
157 |
158 | dt : DateTime
159 | dt =
160 | dateTime { year = 1993, month = 2, day = 31, hour = 0, minute = 0, second = 0, millisecond = 0 }
161 |
162 | year dt --> 1993
163 | month dt --> 2
164 | day dt --> 28 - Note clamped.
165 | hour dt --> 0
166 | minute dt --> 0
167 | second --> 0
168 | millisecond --> 0
169 | ```
170 |
171 | To make constructing `DateTimes` less tedious, the library provides
172 | `Time.DateTime.zero`:
173 |
174 | ``` elm
175 | > import Time.DateTime as DateTime exposing (DateTime, dateTime, zero)
176 |
177 | > dateTime { zero | year = 1992 }
178 | | |> DateTime.toISO8601
179 | "1992-01-01T00:00:00.000Z" : String
180 |
181 | > dateTime { zero | year = 1992, month = 2, day = 28, hour = 5 }
182 | | |> DateTime.toISO8601
183 | "1992-02-28T05:00:00.000Z" : String
184 | ```
185 |
186 | Use `fromTimestamp` to construct a `DateTime` from a UTC timestamp in
187 | milliseconds:
188 |
189 | ``` elm
190 | > fromTimestamp 0
191 | | |> DateTime.toISO8601
192 | "1970-01-01T00:00:00.000Z" : String
193 | ```
194 |
195 | See `examples/without-timezone` for an example of how to construct
196 | `DateTime`s from local time.
197 |
198 | ### Manipulating DateTimes
199 |
200 | Like `Time.Date`, the `DateTime` module exposes functions for adding
201 | to and updating a `DateTime`'s fields. The functions `addYears` and
202 | `addMonths` have the same behaviour as their `Time.Date` counterparts.
203 |
204 | ## ZonedDateTimes
205 |
206 | `ZonedDateTimes` represent a `DateTime` in a specific `TimeZone`. See
207 | `examples/with-timezone` for an example of how to use `ZonedDateTime`s.
208 |
209 | ``` elm
210 | import Time.TimeZones as TimeZones
211 | import Time.ZonedDateTime as ZonedDateTime exposing (ZonedDateTime)
212 | ```
213 |
214 |
215 | [cal]: https://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar
216 |
--------------------------------------------------------------------------------
/src/Time/Iso8601ErrorMsg.elm:
--------------------------------------------------------------------------------
1 | module Time.Iso8601ErrorMsg
2 | exposing
3 | ( reflow
4 | , renderText
5 | )
6 |
7 | {-| A renderer to format error messages resulting from
8 | ISO8601 parsing errors.
9 |
10 | At this time, there is a renderer, **renderText**, to render
11 | a fixed-font message to, say, a terminal screen. This renderer
12 | strives to output the friendly useful error message that elm
13 | is famous for.
14 |
15 |
16 | # Main entry
17 |
18 | @docs renderText
19 |
20 |
21 | # Utilities
22 |
23 | @docs reflow
24 |
25 | -}
26 |
27 | import Parser exposing (Parser, Problem(Fail))
28 |
29 |
30 | {-| Invoking the renderer. This returns an 'elm compiler-style formatted' error string
31 |
32 | import Parser
33 |
34 | -- The \n in the middle of the string is to provide for the 72-char right margin
35 | failString : String
36 | failString =
37 | "Expecting the value 29 to be in the range 1 to 28 for the specified\nyear, 1991, and month, 2."
38 |
39 | renderedString : String
40 | renderedString =
41 | "The 'day-of-month' segment is invalid:\n\n" ++
42 | " 1991-02-29T12:25:12.0Z\n" ++
43 | " ^\n\n" ++
44 | failString
45 |
46 | parserError : Parser.Error
47 | parserError =
48 | { row = 1
49 | , col = 11
50 | , source = "1991-02-29T12:25:12.0Z"
51 | , problem = Parser.Fail failString
52 | , context = [{ row = 1, col = 11, description = "leap-year" }]
53 | }
54 |
55 | renderText parserError
56 | --> renderedString
57 |
58 | -}
59 | renderText : Parser.Error -> String
60 | renderText error =
61 | let
62 | -- Hack: special handling of the leap year.
63 | -- This sets the marker under the beginning of the
64 | -- day-of-month segment.
65 | tweakCol ctx =
66 | if ctx.description == "leap-year" then
67 | ctx.col - 2
68 | else
69 | ctx.col
70 |
71 | ( source, diagnosis, col ) =
72 | case List.head error.context of
73 | Nothing ->
74 | ( Nothing, noContext, error.col )
75 |
76 | Just ctx ->
77 | ( Just ctx.description
78 | , forContext ctx error.problem
79 | , tweakCol ctx
80 | )
81 | in
82 | diagnosis
83 | ++ "\n\n "
84 | ++ relevantSource error
85 | ++ "\n "
86 | ++ marker col
87 | ++ "\n\n"
88 | ++ (reflow <| describeProblem source error.problem)
89 |
90 |
91 | {-| A convenience function to auto-wrap long strings
92 |
93 | Use this method to appropriately wrap the error string returned
94 | from `renderText` above.
95 |
96 | -- \n inserted at 72nt position coz is right margin.
97 | reflow "Expecting the value 29 to be in the range 1 to 28 for the specified year, 1991, and month, 2."
98 | --> "Expecting the value 29 to be in the range 1 to 28 for the specified" ++ "\n" ++ "year, 1991, and month, 2."
99 |
100 | -}
101 | reflow : String -> String
102 | reflow s =
103 | let
104 | flowLine : String -> String
105 | flowLine s =
106 | String.words s
107 | |> makeSentences
108 | |> String.join "\n"
109 |
110 | makeSentences : List String -> List String
111 | makeSentences words =
112 | List.foldl
113 | (\word ( sentence, acc ) ->
114 | let
115 | combined =
116 | case sentence of
117 | Nothing ->
118 | word
119 |
120 | Just s ->
121 | s ++ " " ++ word
122 | in
123 | if String.length combined > 72 then
124 | ( Just word, sentence :: acc )
125 | else
126 | ( Just combined, acc )
127 | )
128 | ( Nothing, [] )
129 | words
130 | |> uncurry (::)
131 | |> reverseFilterMap identity
132 | in
133 | s
134 | |> String.lines
135 | |> List.map flowLine
136 | |> String.join "\n"
137 |
138 |
139 | reverseFilterMap : (a -> Maybe b) -> List a -> List b
140 | reverseFilterMap toMaybe list =
141 | List.foldl
142 | (\x acc ->
143 | case toMaybe x of
144 | Just y ->
145 | y :: acc
146 |
147 | Nothing ->
148 | acc
149 | )
150 | []
151 | list
152 |
153 |
154 | relevantSource : Parser.Error -> String
155 | relevantSource { row, source } =
156 | String.lines source
157 | |> List.drop (row - 1)
158 | |> List.head
159 | |> Maybe.withDefault ""
160 |
161 |
162 | describeProblem : Maybe String -> Parser.Problem -> String
163 | describeProblem probableCause problem =
164 | case problem of
165 | Parser.BadInt ->
166 | "Unable to read an integer here."
167 |
168 | Parser.BadFloat ->
169 | "Unable to read a float here"
170 |
171 | Parser.BadRepeat ->
172 | case probableCause of
173 | Just cause ->
174 | "Can't find a " ++ cause ++ "."
175 |
176 | Nothing ->
177 | "I got stuck here. I'm probably looking for something specific and not making any progress here."
178 |
179 | Parser.ExpectingEnd ->
180 | "String should have stopped here, but it goes on."
181 |
182 | -- Usurped for bad day in month due to leap year:
183 | Parser.ExpectingSymbol s ->
184 | "Expecting a `" ++ s ++ "` here."
185 |
186 | Parser.ExpectingKeyword s ->
187 | "Expecting a keyword `" ++ s ++ "` here."
188 |
189 | Parser.ExpectingVariable ->
190 | "Expecting a variable here."
191 |
192 | Parser.ExpectingClosing s ->
193 | "Expecting a closing `" ++ s ++ "` here."
194 |
195 | Parser.Fail s ->
196 | s
197 |
198 | Parser.BadOneOf problems ->
199 | "Encountering multiple problems:\n\n"
200 | ++ (List.map (describeProblem probableCause) problems |> String.join "\n\n")
201 |
202 |
203 | adjustMarker : Parser.Error -> Parser.Context -> Int
204 | adjustMarker error context =
205 | case error.problem of
206 | Fail msg ->
207 | context.col
208 |
209 | _ ->
210 | error.col
211 |
212 |
213 | marker : Int -> String
214 | marker col =
215 | String.repeat (col - 1) " " ++ "^"
216 |
217 |
218 | forContext : Parser.Context -> Parser.Problem -> String
219 | forContext { description } problem =
220 | let
221 | -- hack; can't find any better way to do this.
222 | segment =
223 | if description == "leap-year" then
224 | "day-of-month"
225 | else
226 | description
227 | in
228 | case problem of
229 | Fail msg ->
230 | "The '" ++ segment ++ "' segment is invalid:"
231 |
232 | _ ->
233 | "Failed to parse the '" ++ segment ++ "' segment:"
234 |
235 |
236 | noContext : String
237 | noContext =
238 | "I ran into a problem parsing this:"
239 |
--------------------------------------------------------------------------------
/tests/TestDate.elm:
--------------------------------------------------------------------------------
1 | module TestDate exposing (..)
2 |
3 | import Expect exposing (Expectation)
4 | import Fuzz exposing (int, intRange)
5 | import Test exposing (..)
6 | import Time.Date exposing (..)
7 |
8 |
9 | someDate : Date
10 | someDate =
11 | date 1992 5 29
12 |
13 |
14 | datesEqual : Date -> ( Int, Int, Int ) -> Expect.Expectation
15 | datesEqual date dateTuple =
16 | Expect.equal (toTuple date) dateTuple
17 |
18 |
19 | validLeapYears : List Int
20 | validLeapYears =
21 | List.concat [ [ -400, -396, -392, -388, -384, -380, -376, -372, -368, -364, -360, -356, -352, -348, -344, -340, -336, -332, -328, -324, -320, -316, -312, -308, -304, -296, -292, -288, -284, -280 ], [ -276, -272, -268, -264, -260, -256, -252, -248, -244, -240, -236, -232, -228, -224, -220, -216, -212, -208, -204, -196, -192, -188, -184, -180, -176, -172, -168, -164, -160, -156 ], [ -152, -148, -144, -140, -136, -132, -128, -124, -120, -116, -112, -108, -104, -96, -92, -88, -84, -80, -76, -72, -68, -64, -60, -56, -52, -48, -44, -40, -36, -32 ], [ -28, -24, -20, -16, -12, -8, -4, 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 68, 72, 76, 80, 84, 88 ], [ 92, 96, 104, 108, 112, 116, 120, 124, 128, 132, 136, 140, 144, 148, 152, 156, 160, 164, 168, 172, 176, 180, 184, 188, 192, 196, 204, 208, 212, 216 ], [ 220, 224, 228, 232, 236, 240, 244, 248, 252, 256, 260, 264, 268, 272, 276, 280, 284, 288, 292, 296, 304, 308, 312, 316, 320, 324, 328, 332, 336, 340 ], [ 344, 348, 352, 356, 360, 364, 368, 372, 376, 380, 384, 388, 392, 396, 400, 404, 408, 412, 416, 420, 424, 428, 432, 436, 440, 444, 448, 452, 456, 460 ], [ 464, 468, 472, 476, 480, 484, 488, 492, 496, 504, 508, 512, 516, 520, 524, 528, 532, 536, 540, 544, 548, 552, 556, 560, 564, 568, 572, 576, 580, 584 ], [ 588, 592, 596, 604, 608, 612, 616, 620, 624, 628, 632, 636, 640, 644, 648, 652, 656, 660, 664, 668, 672, 676, 680, 684, 688, 692, 696, 704, 708, 712 ], [ 716, 720, 724, 728, 732, 736, 740, 744, 748, 752, 756, 760, 764, 768, 772, 776, 780, 784, 788, 792, 796, 800, 804, 808, 812, 816, 820, 824, 828, 832 ], [ 836, 840, 844, 848, 852, 856, 860, 864, 868, 872, 876, 880, 884, 888, 892, 896, 904, 908, 912, 916, 920, 924, 928, 932, 936, 940, 944, 948, 952, 956 ], [ 960, 964, 968, 972, 976, 980, 984, 988, 992, 996, 1004, 1008, 1012, 1016, 1020, 1024, 1028, 1032, 1036, 1040, 1044, 1048, 1052, 1056, 1060, 1064, 1068, 1072, 1076, 1080 ], [ 1084, 1088, 1092, 1096, 1104, 1108, 1112, 1116, 1120, 1124, 1128, 1132, 1136, 1140, 1144, 1148, 1152, 1156, 1160, 1164, 1168, 1172, 1176, 1180, 1184, 1188, 1192, 1196, 1200, 1204 ], [ 1208, 1212, 1216, 1220, 1224, 1228, 1232, 1236, 1240, 1244, 1248, 1252, 1256, 1260, 1264, 1268, 1272, 1276, 1280, 1284, 1288, 1292, 1296, 1304, 1308, 1312, 1316, 1320, 1324, 1328 ], [ 1332, 1336, 1340, 1344, 1348, 1352, 1356, 1360, 1364, 1368, 1372, 1376, 1380, 1384, 1388, 1392, 1396, 1404, 1408, 1412, 1416, 1420, 1424, 1428, 1432, 1436, 1440, 1444, 1448, 1452 ], [ 1456, 1460, 1464, 1468, 1472, 1476, 1480, 1484, 1488, 1492, 1496, 1504, 1508, 1512, 1516, 1520, 1524, 1528, 1532, 1536, 1540, 1544, 1548, 1552, 1556, 1560, 1564, 1568, 1572, 1576 ], [ 1580, 1584, 1588, 1592, 1596, 1600, 1604, 1608, 1612, 1616, 1620, 1624, 1628, 1632, 1636, 1640, 1644, 1648, 1652, 1656, 1660, 1664, 1668, 1672, 1676, 1680, 1684, 1688, 1692, 1696 ], [ 1704, 1708, 1712, 1716, 1720, 1724, 1728, 1732, 1736, 1740, 1744, 1748, 1752, 1756, 1760, 1764, 1768, 1772, 1776, 1780, 1784, 1788, 1792, 1796, 1804, 1808, 1812, 1816, 1820, 1824 ], [ 1828, 1832, 1836, 1840, 1844, 1848, 1852, 1856, 1860, 1864, 1868, 1872, 1876, 1880, 1884, 1888, 1892, 1896, 1904, 1908, 1912, 1916, 1920, 1924, 1928, 1932, 1936, 1940, 1944, 1948 ], [ 1952, 1956, 1960, 1964, 1968, 1972, 1976, 1980, 1984, 1988, 1992, 1996, 2000, 2004, 2008, 2012, 2016, 2020 ] ]
22 |
23 |
24 | standardYearMonths : List Int
25 | standardYearMonths =
26 | [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ]
27 |
28 |
29 | leapYearMonths : List Int
30 | leapYearMonths =
31 | [ 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ]
32 |
33 |
34 | monthDays : Int -> Int -> Int
35 | monthDays year month =
36 | let
37 | monthDays =
38 | if isLeapYear year then
39 | leapYearMonths
40 | else
41 | standardYearMonths
42 | in
43 | monthDays
44 | |> List.drop (month - 1)
45 | |> List.head
46 | |> Maybe.withDefault 0
47 |
48 |
49 | fuzzDate : String -> (Int -> Int -> Int -> Expectation) -> Test
50 | fuzzDate =
51 | fuzz3 int (intRange 1 12) (intRange 1 31)
52 |
53 |
54 | constructing : Test
55 | constructing =
56 | describe "Time.Date.date"
57 | [ fuzzDate "constructs dates" <|
58 | \year month day ->
59 | let
60 | day_ =
61 | clamp 1 day (monthDays year month)
62 | in
63 | datesEqual (date year month day_) ( year, month, day_ )
64 | , test "constructs valid dates" <|
65 | always <|
66 | datesEqual (date 1992 5 29) ( 1992, 5, 29 )
67 | , test "accounts for leap years" <|
68 | always <|
69 | datesEqual (date 1992 2 29) ( 1992, 2, 29 )
70 | , test "clamps invalid dates" <|
71 | always <|
72 | datesEqual (date 1993 2 29) ( 1993, 2, 28 )
73 | ]
74 |
75 |
76 | leapYears : Test
77 | leapYears =
78 | describe "Time.Date.isLeapYear"
79 | [ describe "isLeapYear"
80 | [ fuzz (intRange -400 2020) "is correct given any year" <|
81 | \year ->
82 | if List.member year validLeapYears then
83 | Expect.true ("Expected " ++ toString year ++ " to be a leap year") (isLeapYear year)
84 | else
85 | Expect.false (toString year ++ " is not a leap year") (isLeapYear year)
86 | ]
87 | , describe "daysInMonth"
88 | [ fuzz2 (intRange -400 2020) (intRange 1 12) "is correct given any year, month pair" <|
89 | \year month ->
90 | daysInMonth year month
91 | |> Expect.equal (Just <| monthDays year month)
92 | ]
93 | ]
94 |
95 |
96 | adders : Test
97 | adders =
98 | describe "Time.Date.add{Years,Months,Days}"
99 | [ test "addYears is relative" <|
100 | \() ->
101 | let
102 | date1 =
103 | date 1992 2 29
104 | |> addYears 1
105 |
106 | date2 =
107 | date 1993 2 28
108 | in
109 | Expect.equal date1 date2
110 | , test "addMonths is relative" <|
111 | \() ->
112 | let
113 | date1 =
114 | date 1992 1 31
115 | |> addMonths 1
116 |
117 | date2 =
118 | date 1992 2 29
119 | in
120 | Expect.equal date1 date2
121 | , fuzz3 (intRange -400 3000) (intRange 1 12) (intRange -100 100) "addMonths is revertable" <|
122 | \year month addage ->
123 | let
124 | date1 =
125 | date year month 1
126 |
127 | date2 =
128 | addMonths addage date1
129 | in
130 | Expect.equal date1 (addMonths -addage date2)
131 | , fuzz int "addDays is absolute" <|
132 | \days ->
133 | let
134 | date1 =
135 | someDate
136 |
137 | date2 =
138 | addDays days date1
139 |
140 | dt =
141 | delta date2 date1
142 | in
143 | Expect.equal days dt.days
144 | ]
145 |
146 |
147 | toFromTuple : Test
148 | toFromTuple =
149 | describe "Time.Date.{to,from}Tuple"
150 | [ fuzzDate "they have an inverse relationship" <|
151 | \year month day ->
152 | let
153 | date1 =
154 | date year month day
155 |
156 | date2 =
157 | date1
158 | |> toTuple
159 | |> fromTuple
160 | in
161 | Expect.equal date1 date2
162 | ]
163 |
--------------------------------------------------------------------------------
/tests/TestDateTime.elm:
--------------------------------------------------------------------------------
1 | module TestDateTime exposing (..)
2 |
3 | import Expect exposing (Expectation)
4 | import Fuzz exposing (Fuzzer, constant, int, intRange, oneOf)
5 | import Parser exposing (Context, Error, Parser, Problem)
6 | import Test exposing (..)
7 | import Time.Date as Date
8 | import Time.DateTime as DateTime
9 | exposing
10 | ( DateTime
11 | , addHours
12 | , addMilliseconds
13 | , addMinutes
14 | , addSeconds
15 | , date
16 | , dateTime
17 | , day
18 | , epoch
19 | , hour
20 | , millisecond
21 | , minute
22 | , month
23 | , second
24 | , setDay
25 | , setHour
26 | , setMillisecond
27 | , setMinute
28 | , setMonth
29 | , setSecond
30 | , setYear
31 | , toTuple
32 | , year
33 | , zero
34 | )
35 | import Time.Iso8601 as Iso8601
36 |
37 |
38 | posInt : Fuzzer Int
39 | posInt =
40 | Fuzz.map abs int
41 |
42 |
43 | dateTimesEqual : DateTime -> ( Int, Int, Int, Int, Int, Int, Int ) -> Expect.Expectation
44 | dateTimesEqual dateTime dateTimeTuple =
45 | Expect.equal (toTuple dateTime) dateTimeTuple
46 |
47 |
48 | dateTimes : Test
49 | dateTimes =
50 | describe "Time.DateTime.{dateTime,epoch}"
51 | [ test "epoch is the epoch" <|
52 | \() ->
53 | Expect.equal
54 | epoch
55 | (dateTime { zero | year = 1970 })
56 | , test "can construt valid dates" <|
57 | \() ->
58 | let
59 | (( year, month, day, hour, minute, second, millisecond ) as dateTimeTuple) =
60 | ( 1992, 5, 29, 23, 29, 50, 920 )
61 | in
62 | dateTimesEqual
63 | (dateTime { year = year, month = month, day = day, hour = hour, minute = minute, second = second, millisecond = millisecond })
64 | dateTimeTuple
65 | , test "clamps invalid dates" <|
66 | \() ->
67 | dateTimesEqual
68 | (dateTime { zero | year = 1993, month = 2, day = 29 })
69 | ( 1993, 2, 28, 0, 0, 0, 0 )
70 | ]
71 |
72 |
73 | setters : Test
74 | setters =
75 | describe "Time.DateTime.set{Year,Month,Day,Hour,Minute,Second,Millisecond}"
76 | [ test "can set year" <|
77 | \() ->
78 | dateTime zero
79 | |> setYear 1992
80 | |> year
81 | |> Expect.equal 1992
82 | , test "can set month" <|
83 | \() ->
84 | dateTime zero
85 | |> setMonth 5
86 | |> month
87 | |> Expect.equal 5
88 | , test "can set month given the current year and day" <|
89 | \() ->
90 | dateTime { zero | year = 1992, month = 1, day = 29 }
91 | |> setMonth 2
92 | |> month
93 | |> Expect.equal 2
94 | , test "can set day" <|
95 | \() ->
96 | dateTime zero
97 | |> setDay 15
98 | |> day
99 | |> Expect.equal 15
100 | , test "can set day given the current year and month" <|
101 | \() ->
102 | dateTime { zero | year = 1992, month = 2 }
103 | |> setDay 29
104 | |> day
105 | |> Expect.equal 29
106 | , test "can set hour" <|
107 | \() ->
108 | dateTime zero
109 | |> setHour 23
110 | |> hour
111 | |> Expect.equal 23
112 | , test "can set minute" <|
113 | \() ->
114 | dateTime zero
115 | |> setMinute 23
116 | |> minute
117 | |> Expect.equal 23
118 | , test "can set second" <|
119 | \() ->
120 | dateTime zero
121 | |> setSecond 23
122 | |> second
123 | |> Expect.equal 23
124 | , test "can set millisecond" <|
125 | \() ->
126 | dateTime zero
127 | |> setMillisecond 230
128 | |> millisecond
129 | |> Expect.equal 230
130 | , test "invalid months are clamped" <|
131 | \() ->
132 | dateTime zero
133 | |> setMonth 31
134 | |> month
135 | |> Expect.equal 12
136 | , test "invalid days are clamped" <|
137 | \() ->
138 | dateTime zero
139 | |> setDay 105
140 | |> day
141 | |> Expect.equal 31
142 | , test "invalid months given the current year and day are clamped to the nearest valid date" <|
143 | \() ->
144 | dateTime { zero | year = 1993, month = 1, day = 31 }
145 | |> setMonth 2
146 | |> date
147 | |> Expect.equal (Date.date 1993 2 28)
148 | , test "invalid days given the current year and month are clamped to the nearest valid date" <|
149 | \() ->
150 | dateTime { zero | year = 1991, month = 2 }
151 | |> setDay 29
152 | |> date
153 | |> Expect.equal (Date.date 1991 2 28)
154 | , fuzz4 int int int int "invalid times are clamped" <|
155 | \hour minute second millisecond ->
156 | dateTime zero
157 | |> setHour hour
158 | |> setMinute minute
159 | |> setSecond second
160 | |> setMillisecond millisecond
161 | |> (\d -> ( DateTime.hour d, DateTime.minute d, DateTime.second d, DateTime.millisecond d ))
162 | |> Expect.equal ( clamp 0 23 hour, clamp 0 59 minute, clamp 0 59 second, clamp 0 999 millisecond )
163 | ]
164 |
165 |
166 | addition : Test
167 | addition =
168 | describe "Time.DateTime.add{Hours,Minutes,Seconds,Milliseconds}"
169 | [ test "can add a positive number of hours" <|
170 | \() ->
171 | epoch
172 | |> addHours 26
173 | |> toTuple
174 | |> Expect.equal ( 1970, 1, 2, 2, 0, 0, 0 )
175 | , test "can add a negative number of hours" <|
176 | \() ->
177 | epoch
178 | |> addHours -24
179 | |> toTuple
180 | |> Expect.equal ( 1969, 12, 31, 0, 0, 0, 0 )
181 | , test "can add a positive number of minutes" <|
182 | \() ->
183 | epoch
184 | |> addMinutes 5
185 | |> toTuple
186 | |> Expect.equal ( 1970, 1, 1, 0, 5, 0, 0 )
187 | , test "can add a negative number of minutes" <|
188 | \() ->
189 | epoch
190 | |> addMinutes -1
191 | |> toTuple
192 | |> Expect.equal ( 1969, 12, 31, 23, 59, 0, 0 )
193 | , test "can add a positive number of seconds" <|
194 | \() ->
195 | epoch
196 | |> addSeconds 30
197 | |> toTuple
198 | |> Expect.equal ( 1970, 1, 1, 0, 0, 30, 0 )
199 | , test "can add a positive number of seconds as an absolute quantity" <|
200 | \() ->
201 | epoch
202 | |> addSeconds 3600
203 | |> toTuple
204 | |> Expect.equal ( 1970, 1, 1, 1, 0, 0, 0 )
205 | , test "can add a negative number of seconds" <|
206 | \() ->
207 | epoch
208 | |> addSeconds -1
209 | |> toTuple
210 | |> Expect.equal ( 1969, 12, 31, 23, 59, 59, 0 )
211 | , test "can add a positive number of milliseconds" <|
212 | \() ->
213 | epoch
214 | |> addMilliseconds 30
215 | |> toTuple
216 | |> Expect.equal ( 1970, 1, 1, 0, 0, 0, 30 )
217 | , test "can add a positive number of milliseconds as an absolute quantity" <|
218 | \() ->
219 | epoch
220 | |> addMilliseconds 86400000
221 | |> toTuple
222 | |> Expect.equal ( 1970, 1, 2, 0, 0, 0, 0 )
223 | , test "can add a negative number of milliseconds" <|
224 | \() ->
225 | epoch
226 | |> addMilliseconds -1
227 | |> toTuple
228 | |> Expect.equal ( 1969, 12, 31, 23, 59, 59, 999 )
229 | ]
230 |
--------------------------------------------------------------------------------
/src/Time/ZonedDateTime.elm:
--------------------------------------------------------------------------------
1 | module Time.ZonedDateTime
2 | exposing
3 | ( ZonedDateTime
4 | , abbreviation
5 | , addDays
6 | , addHours
7 | , addMilliseconds
8 | , addMinutes
9 | , addMonths
10 | , addSeconds
11 | , addYears
12 | , asTimeZone
13 | , day
14 | , fromDateTime
15 | , fromTimestamp
16 | , hour
17 | , millisecond
18 | , minute
19 | , month
20 | , second
21 | , setDate
22 | , setDay
23 | , setHour
24 | , setMillisecond
25 | , setMinute
26 | , setMonth
27 | , setSecond
28 | , setYear
29 | , timeZone
30 | , toDateTime
31 | , toTimestamp
32 | , utcOffset
33 | , utcOffsetString
34 | , weekday
35 | , year
36 | , zero
37 | , zonedDateTime
38 | )
39 |
40 | {-| This module defines a time representation based on a Date, the
41 | time of day and a time zone.
42 |
43 | ZonedDateTimes should only be used when reasoning about or displaying
44 | `DateTime`s in a user's local time zone. For this reason, the API
45 | surface of `ZonedDateTimes` is extremely limited.
46 |
47 |
48 | # ZonedDateTimes
49 |
50 | @docs ZonedDateTime
51 |
52 |
53 | # Constructing ZonedDateTimes
54 |
55 | @docs zero, zonedDateTime, fromDateTime, toDateTime, fromTimestamp, toTimestamp
56 |
57 |
58 | # Inspecting ZonedDateTimes
59 |
60 | @docs timeZone, year, month, day, weekday, hour, minute, second, millisecond, abbreviation, utcOffset, utcOffsetString
61 |
62 |
63 | # Manipulating ZonedDateTimes
64 |
65 | @docs asTimeZone, setDate, setYear, setMonth, setDay, setHour, setMinute, setSecond, setMillisecond, addYears, addMonths, addDays, addHours, addMinutes, addSeconds, addMilliseconds
66 |
67 | -}
68 |
69 | import Time exposing (Time)
70 | import Time.Date exposing (Date, Weekday)
71 | import Time.DateTime as DateTime exposing (DateTime)
72 | import Time.Internal exposing (..)
73 | import Time.TimeZone as TimeZone exposing (TimeZone)
74 |
75 |
76 | {-| ZoneDateTime is the opaque type for all ZonedDateTime values.
77 | Values of this type represent a `(TimeZone, DateTime)` pair.
78 | -}
79 | type ZonedDateTime
80 | = ZonedDateTime
81 | { timeZone : TimeZone
82 | , dateTime : DateTime
83 | }
84 |
85 |
86 | {-| zero represents the first millisecond of the first day of the
87 | current era. Use it to build `ZonedDateTime` values:
88 |
89 | -- 0-01-01T00:00:00+02:00
90 | zonedDateTime (europe_bucharest ()) zero
91 |
92 | -- 2016-01-01T00:00:00+02:00
93 | zonedDateTime (europe_bucharest ()) { zero | year = 2016 }
94 |
95 | -- 2016-05-29T13:00:00+02:00
96 | zonedDateTime (europe_bucharest ()) { zero | year = 2016, month = 5, day = 29, hour = 13 }
97 |
98 | -}
99 | zero : DateTimeData
100 | zero =
101 | Time.Internal.zero
102 |
103 |
104 | {-| zonedDateTime constructs a ZonedDateTime value given a TimeZone, a
105 | date and a time. Invalid values are clamped to the nearest valid date
106 | and time.
107 | -}
108 | zonedDateTime : TimeZone -> DateTimeData -> ZonedDateTime
109 | zonedDateTime timeZone dateTimeData =
110 | ZonedDateTime
111 | { timeZone = timeZone
112 | , dateTime = DateTime.dateTime dateTimeData
113 | }
114 |
115 |
116 | {-| fromDateTime constructs a ZonedDateTime value from a TimeZone and
117 | a DateTime.
118 | -}
119 | fromDateTime : TimeZone -> DateTime -> ZonedDateTime
120 | fromDateTime timeZone dateTime =
121 | let
122 | timestamp =
123 | DateTime.toTimestamp dateTime
124 |
125 | offset =
126 | TimeZone.offset timestamp timeZone
127 | in
128 | ZonedDateTime
129 | { timeZone = timeZone
130 | , dateTime = DateTime.addMilliseconds -offset dateTime
131 | }
132 |
133 |
134 | {-| toDateTime converts a ZonedDateTime to a UTC DateTime value.
135 | -}
136 | toDateTime : ZonedDateTime -> DateTime
137 | toDateTime ((ZonedDateTime { dateTime }) as zonedDateTime) =
138 | utcOffset zonedDateTime
139 | |> flip DateTime.addMilliseconds dateTime
140 |
141 |
142 | {-| fromTimestamp converts the millisecond representation of a UNIX
143 | timestamp into a ZonedDateTime value. This is equivalent to calling
144 | `DateTime.fromTimestamp` and then converting the resulting `DateTime`
145 | value to a `ZonedDateTime`.
146 | -}
147 | fromTimestamp : TimeZone -> Time -> ZonedDateTime
148 | fromTimestamp timeZone timestamp =
149 | DateTime.fromTimestamp timestamp
150 | |> fromDateTime timeZone
151 |
152 |
153 | {-| toTimestamp converts a ZonedDateTime to its UNIX timestamp
154 | representation in milliseconds.
155 | -}
156 | toTimestamp : ZonedDateTime -> Time
157 | toTimestamp (ZonedDateTime { timeZone, dateTime }) =
158 | DateTime.toTimestamp dateTime
159 | |> flip TimeZone.offset timeZone
160 | |> flip DateTime.addMilliseconds dateTime
161 | |> DateTime.toTimestamp
162 |
163 |
164 | {-| timeZone returns a ZonedDatetime's TimeZone.
165 | -}
166 | timeZone : ZonedDateTime -> TimeZone
167 | timeZone (ZonedDateTime { timeZone }) =
168 | timeZone
169 |
170 |
171 | {-| asTimeZone converts a ZonedDateTime to another TimeZone.
172 | -}
173 | asTimeZone : TimeZone -> ZonedDateTime -> ZonedDateTime
174 | asTimeZone timeZone =
175 | fromDateTime timeZone << toDateTime
176 |
177 |
178 | {-| year returns a ZonedDateTime's year.
179 | -}
180 | year : ZonedDateTime -> Int
181 | year (ZonedDateTime { dateTime }) =
182 | DateTime.year dateTime
183 |
184 |
185 | {-| month returns a ZonedDateTime's month.
186 | -}
187 | month : ZonedDateTime -> Int
188 | month (ZonedDateTime { dateTime }) =
189 | DateTime.month dateTime
190 |
191 |
192 | {-| day returns a ZonedDateTime's day.
193 | -}
194 | day : ZonedDateTime -> Int
195 | day (ZonedDateTime { dateTime }) =
196 | DateTime.day dateTime
197 |
198 |
199 | {-| weekday returns a ZonedDateTime's day of the week.
200 | -}
201 | weekday : ZonedDateTime -> Weekday
202 | weekday (ZonedDateTime { dateTime }) =
203 | DateTime.weekday dateTime
204 |
205 |
206 | {-| hour returns a ZonedDateTime's hour.
207 | -}
208 | hour : ZonedDateTime -> Int
209 | hour (ZonedDateTime { dateTime }) =
210 | DateTime.hour dateTime
211 |
212 |
213 | {-| minute returns a ZonedDateTime's minute.
214 | -}
215 | minute : ZonedDateTime -> Int
216 | minute (ZonedDateTime { dateTime }) =
217 | DateTime.minute dateTime
218 |
219 |
220 | {-| second returns a ZonedDateTime's second.
221 | -}
222 | second : ZonedDateTime -> Int
223 | second (ZonedDateTime { dateTime }) =
224 | DateTime.second dateTime
225 |
226 |
227 | {-| millisecond returns a ZonedDateTime's millisecond.
228 | -}
229 | millisecond : ZonedDateTime -> Int
230 | millisecond (ZonedDateTime { dateTime }) =
231 | DateTime.millisecond dateTime
232 |
233 |
234 | mapInner : (a -> DateTime -> DateTime) -> a -> ZonedDateTime -> ZonedDateTime
235 | mapInner f x (ZonedDateTime ({ dateTime } as t)) =
236 | ZonedDateTime { t | dateTime = f x dateTime }
237 |
238 |
239 | {-| setDate sets a ZonedDateTime's date.
240 | -}
241 | setDate : Date -> ZonedDateTime -> ZonedDateTime
242 | setDate =
243 | mapInner DateTime.setDate
244 |
245 |
246 | {-| setYear sets a ZonedDateTime's year.
247 | -}
248 | setYear : Int -> ZonedDateTime -> ZonedDateTime
249 | setYear =
250 | mapInner DateTime.setYear
251 |
252 |
253 | {-| setMonth sets a ZonedDateTime's month.
254 | -}
255 | setMonth : Int -> ZonedDateTime -> ZonedDateTime
256 | setMonth =
257 | mapInner DateTime.setMonth
258 |
259 |
260 | {-| setDay sets a ZonedDateTime's day.
261 | -}
262 | setDay : Int -> ZonedDateTime -> ZonedDateTime
263 | setDay =
264 | mapInner DateTime.setDay
265 |
266 |
267 | {-| setHour sets a ZonedDateTime's hour.
268 | -}
269 | setHour : Int -> ZonedDateTime -> ZonedDateTime
270 | setHour =
271 | mapInner DateTime.setHour
272 |
273 |
274 | {-| setMinute sets a ZonedDateTime's minute.
275 | -}
276 | setMinute : Int -> ZonedDateTime -> ZonedDateTime
277 | setMinute =
278 | mapInner DateTime.setMinute
279 |
280 |
281 | {-| setSecond sets a ZonedDateTime's second.
282 | -}
283 | setSecond : Int -> ZonedDateTime -> ZonedDateTime
284 | setSecond =
285 | mapInner DateTime.setSecond
286 |
287 |
288 | {-| setMillisecond sets a ZonedDateTime's millisecond.
289 | -}
290 | setMillisecond : Int -> ZonedDateTime -> ZonedDateTime
291 | setMillisecond =
292 | mapInner DateTime.setMillisecond
293 |
294 |
295 | {-| addYears adds a relative number of years to a ZonedDateTime value.
296 | -}
297 | addYears : Int -> ZonedDateTime -> ZonedDateTime
298 | addYears =
299 | mapInner DateTime.addYears
300 |
301 |
302 | {-| addMonths adds a relative number of months to a ZonedDateTime value.
303 | -}
304 | addMonths : Int -> ZonedDateTime -> ZonedDateTime
305 | addMonths =
306 | mapInner DateTime.addMonths
307 |
308 |
309 | {-| addDays adds an absolute number of days to a ZonedDateTime value.
310 | -}
311 | addDays : Int -> ZonedDateTime -> ZonedDateTime
312 | addDays =
313 | mapInner DateTime.addDays
314 |
315 |
316 | {-| addHours adds a relative number of hours to a ZonedDateTime value.
317 | -}
318 | addHours : Int -> ZonedDateTime -> ZonedDateTime
319 | addHours =
320 | mapInner DateTime.addHours
321 |
322 |
323 | {-| addMinutes adds a relative number of minutes to a ZonedDateTime value.
324 | -}
325 | addMinutes : Int -> ZonedDateTime -> ZonedDateTime
326 | addMinutes =
327 | mapInner DateTime.addMinutes
328 |
329 |
330 | {-| addSeconds adds a relative number of seconds to a ZonedDateTime value.
331 | -}
332 | addSeconds : Int -> ZonedDateTime -> ZonedDateTime
333 | addSeconds =
334 | mapInner DateTime.addSeconds
335 |
336 |
337 | {-| addMilliseconds adds an absolute number of milliseconds to a
338 | ZonedDateTime value.
339 | -}
340 | addMilliseconds : Int -> ZonedDateTime -> ZonedDateTime
341 | addMilliseconds =
342 | mapInner DateTime.addMilliseconds
343 |
344 |
345 | {-| abbreviation returns a ZonedDateTime's abbreviation at that time.
346 | -}
347 | abbreviation : ZonedDateTime -> String
348 | abbreviation ((ZonedDateTime { timeZone }) as zonedDateTime) =
349 | toTimestamp zonedDateTime
350 | |> flip TimeZone.abbreviation timeZone
351 |
352 |
353 | {-| utcOffset returns a ZonedDateTime's offset from UTC in
354 | milliseconds at that time.
355 | -}
356 | utcOffset : ZonedDateTime -> Int
357 | utcOffset ((ZonedDateTime { timeZone }) as zonedDateTime) =
358 | toTimestamp zonedDateTime
359 | |> flip TimeZone.offset timeZone
360 |
361 |
362 | {-| utcOffsetString returns a ZonedDateTime's UTC offset at that time
363 | as a string.
364 | -}
365 | utcOffsetString : ZonedDateTime -> String
366 | utcOffsetString ((ZonedDateTime { timeZone }) as zonedDateTime) =
367 | toTimestamp zonedDateTime
368 | |> flip TimeZone.offsetString timeZone
369 |
--------------------------------------------------------------------------------
/src/Time/Iso8601.elm:
--------------------------------------------------------------------------------
1 | module Time.Iso8601
2 | exposing
3 | ( fromDate
4 | , fromDateTime
5 | , fromZonedDateTime
6 | , toDate
7 | , toDateTime
8 | , toZonedDateTime
9 | )
10 |
11 | {-|
12 |
13 |
14 | # Render ISO8601 strings
15 |
16 | @docs fromDate, fromDateTime, fromZonedDateTime
17 |
18 |
19 | # Parse ISO8601 strings
20 |
21 | @docs toDate, toDateTime, toZonedDateTime
22 |
23 | -}
24 |
25 | import Char
26 | import Parser
27 | exposing
28 | ( (|.)
29 | , (|=)
30 | , Count(..)
31 | , Error
32 | , Parser
33 | , andThen
34 | , fail
35 | , ignore
36 | , inContext
37 | , keep
38 | , oneOf
39 | , oneOrMore
40 | , run
41 | , succeed
42 | , zeroOrMore
43 | )
44 | import Time.Date as Date
45 | exposing
46 | ( Date
47 | , daysInMonth
48 | , isValidDate
49 | )
50 | import Time.DateTime as DateTime
51 | exposing
52 | ( DateTime
53 | , addMilliseconds
54 | , dateTime
55 | , day
56 | , hour
57 | , makeDateTime
58 | , millisecond
59 | , minute
60 | , month
61 | , second
62 | , year
63 | )
64 | import Time.Internal
65 | exposing
66 | ( hourMs
67 | , minuteMs
68 | , offsetFromTimeData
69 | , padded
70 | , padded3
71 | )
72 | import Time.TimeZone exposing (TimeZone)
73 | import Time.ZonedDateTime as ZDT exposing (ZonedDateTime)
74 |
75 |
76 | -- Shared parsers
77 | -- --------------
78 |
79 |
80 | {-| Offset is expressed in +/- milliseconds
81 | -}
82 | type alias Milliseconds =
83 | Int
84 |
85 |
86 | {-| fromDate renders a Date in ISO8601 format.
87 |
88 | import Time.Date exposing (..)
89 |
90 | date 2018 5 27
91 | |> fromDate
92 | --> "2018-05-27"
93 |
94 | -}
95 | fromDate : Date -> String
96 | fromDate date =
97 | (toString (Date.year date) |> String.padLeft 4 '0')
98 | ++ "-"
99 | ++ padded (Date.month date)
100 | ++ "-"
101 | ++ padded (Date.day date)
102 |
103 |
104 | {-| fromDateTime renders a DateTime in ISO8601 format.
105 |
106 | import Time.DateTime exposing (..)
107 |
108 | epoch
109 | |> addMilliseconds 61000
110 | |> fromDateTime
111 | --> "1970-01-01T00:01:01.000Z"
112 |
113 | -}
114 | fromDateTime : DateTime -> String
115 | fromDateTime time =
116 | toString (year time)
117 | ++ "-"
118 | ++ padded (month time)
119 | ++ "-"
120 | ++ padded (day time)
121 | ++ "T"
122 | ++ padded (hour time)
123 | ++ ":"
124 | ++ padded (minute time)
125 | ++ ":"
126 | ++ padded (second time)
127 | ++ "."
128 | ++ padded3 (millisecond time)
129 | ++ "Z"
130 |
131 |
132 | {-| fromZonedDateTime renders a ZonedDateTime in ISO8601 format.
133 |
134 | import Time.ZonedDateTime
135 | import Time.TimeZones exposing (america_new_york)
136 | import Time.DateTime exposing (epoch)
137 |
138 | Time.ZonedDateTime.fromDateTime (america_new_york ()) epoch
139 | |> fromZonedDateTime
140 | --> "1969-12-31T19:00:00.000-05:00"
141 |
142 | -}
143 | fromZonedDateTime : ZonedDateTime -> String
144 | fromZonedDateTime dateTime =
145 | toString (ZDT.year dateTime)
146 | ++ "-"
147 | ++ padded (ZDT.month dateTime)
148 | ++ "-"
149 | ++ padded (ZDT.day dateTime)
150 | ++ "T"
151 | ++ padded (ZDT.hour dateTime)
152 | ++ ":"
153 | ++ padded (ZDT.minute dateTime)
154 | ++ ":"
155 | ++ padded (ZDT.second dateTime)
156 | ++ "."
157 | ++ padded3 (ZDT.millisecond dateTime)
158 | ++ ZDT.utcOffsetString dateTime
159 |
160 |
161 | {-| toZonedDateTime parses an ISO8601-formatted string into a
162 | ZonedDateTime object, adjusting for its offset.
163 |
164 | import Time.ZonedDateTime
165 | import Time.TimeZones exposing (america_new_york)
166 | import Time.DateTime exposing (epoch)
167 |
168 | toZonedDateTime (america_new_york ()) "1970-01-01T00:00:00.000Z"
169 | --> Ok (Time.ZonedDateTime.fromDateTime (america_new_york ()) epoch)
170 |
171 | -}
172 | toZonedDateTime : TimeZone -> String -> Result Parser.Error ZonedDateTime
173 | toZonedDateTime timeZone input =
174 | toDateTime input
175 | |> Result.map (ZDT.fromDateTime timeZone)
176 |
177 |
178 | {-| toDate parses an ISO8601-formatted date string into a Date.
179 |
180 | import Time.Date
181 |
182 | toDate "1970-12-01"
183 | --> Ok (Time.Date.date 1970 12 1)
184 |
185 | toDate "19701201"
186 | --> Ok (Time.Date.date 1970 12 1)
187 |
188 | -}
189 | toDate : String -> Result Parser.Error Date
190 | toDate input =
191 | run parseDate input
192 |
193 |
194 | {-| -}
195 | parseDate : Parser Date
196 | parseDate =
197 | (succeed (,,)
198 | |= parseYear
199 | |. optional '-'
200 | |= parseMonth
201 | |. optional '-'
202 | |= parseDay
203 | )
204 | |> andThen convertDate
205 |
206 |
207 | parseYear : Parser Int
208 | parseYear =
209 | digits "year" 4
210 |
211 |
212 | parseMonth : Parser Int
213 | parseMonth =
214 | digitsInRange "month" 2 1 12
215 |
216 |
217 | parseDay : Parser Int
218 | parseDay =
219 | digitsInRange "day-in-month" 2 1 31
220 |
221 |
222 | digits : String -> Int -> Parser Int
223 | digits name digitsCount =
224 | inContext name <|
225 | (keep (Exactly digitsCount) Char.isDigit
226 | |> andThen (fromResult << String.toInt)
227 | )
228 |
229 |
230 | convertDate : ( Int, Int, Int ) -> Parser Date
231 | convertDate ( year, month, day ) =
232 | if isValidDate year month day then
233 | succeed (Date.date year month day)
234 | else
235 | complainInvalid ( year, month, day )
236 |
237 |
238 | complainInvalid : ( Int, Int, Int ) -> Parser Date
239 | complainInvalid ( year, month, day ) =
240 | inContext "leap-year" <|
241 | let
242 | maxDays =
243 | Maybe.withDefault 31 (daysInMonth year month)
244 |
245 | msg =
246 | "Expecting the value "
247 | ++ toString day
248 | ++ " to be in the range 1 to "
249 | ++ toString maxDays
250 | ++ " for the specified year, "
251 | ++ toString year
252 | ++ ", and month, "
253 | ++ toString month
254 | ++ "."
255 | in
256 | fail msg
257 |
258 |
259 | digitsInRange : String -> Int -> Int -> Int -> Parser Int
260 | digitsInRange name digitsCount lo hi =
261 | inContext name <|
262 | (keep (Exactly digitsCount) (\c -> Char.isDigit c)
263 | |> andThen (intRange lo hi << String.toInt)
264 | )
265 |
266 |
267 | intRange : Int -> Int -> Result String Int -> Parser Int
268 | intRange lo hi result =
269 | case result of
270 | Ok n ->
271 | if n >= lo && n <= hi then
272 | succeed n
273 | else
274 | fail
275 | ("Expecting the value "
276 | ++ toString n
277 | ++ " to be in the range "
278 | ++ toString lo
279 | ++ " to "
280 | ++ toString hi
281 | ++ "."
282 | )
283 |
284 | Err msg ->
285 | Parser.fail msg
286 |
287 |
288 | fromResult : Result String Int -> Parser Int
289 | fromResult result =
290 | case result of
291 | Ok i ->
292 | succeed i
293 |
294 | Err msg ->
295 | fail msg
296 |
297 |
298 | optional : Char -> Parser ()
299 | optional char =
300 | ignore zeroOrMore (\c -> c == char)
301 |
302 |
303 | {-| toDateTime parses an ISO8601-formatted date time string into a
304 | DateTime object, adjusting for its timezone offset.
305 | -}
306 | toDateTime : String -> Result Parser.Error DateTime
307 | toDateTime input =
308 | run parseDateTime input
309 |
310 |
311 | parseDateTime : Parser DateTime
312 | parseDateTime =
313 | (succeed (,,)
314 | |= parseDate
315 | |. optional 'T'
316 | |= parseOffset
317 | |= tZOffset
318 | )
319 | |> andThen convertDateTime
320 |
321 |
322 | parseOffset : Parser Milliseconds
323 | parseOffset =
324 | (succeed (,,,)
325 | |= digitsInRange "hours" 2 0 23
326 | |. optional ':'
327 | |= digitsInRange "minutes" 2 0 59
328 | |. optional ':'
329 | |= digitsInRange "seconds" 2 0 59
330 | |= fraction
331 | )
332 | |> andThen convertTime
333 |
334 |
335 | convertDateTime : ( Date, Milliseconds, Milliseconds ) -> Parser DateTime
336 | convertDateTime ( date, offset, tZOffset ) =
337 | succeed
338 | (makeDateTime
339 | date
340 | offset
341 | |> addMilliseconds tZOffset
342 | )
343 |
344 |
345 | convertTime : ( Int, Int, Int, Int ) -> Parser Milliseconds
346 | convertTime ( hours, minutes, seconds, milliseconds ) =
347 | succeed
348 | (offsetFromTimeData
349 | { hour = hours
350 | , minute = minutes
351 | , second = seconds
352 | , millisecond = milliseconds
353 | }
354 | )
355 |
356 |
357 | fraction : Parser Milliseconds
358 | fraction =
359 | oneOf
360 | [ optionalFraction
361 | , succeed 0
362 | ]
363 |
364 |
365 | optionalFraction : Parser Milliseconds
366 | optionalFraction =
367 | inContext "fraction" <|
368 | ((succeed identity
369 | |. keep (Exactly 1) ((==) '.')
370 | |= keep oneOrMore Char.isDigit
371 | )
372 | |> andThen (fromResult << getFraction)
373 | )
374 |
375 |
376 | getFraction : String -> Result String Milliseconds
377 | getFraction fractionString =
378 | let
379 | numerator =
380 | Result.withDefault 0 (String.toInt fractionString)
381 |
382 | denominator =
383 | 10 ^ String.length fractionString
384 | in
385 | Ok (round (Time.Internal.secondMs * toFloat numerator / toFloat denominator))
386 |
387 |
388 | tZOffset : Parser Milliseconds
389 | tZOffset =
390 | oneOf
391 | [ utc
392 | , optionalTZOffset
393 | , succeed 0
394 | ]
395 |
396 |
397 | utc : Parser Milliseconds
398 | utc =
399 | (succeed identity
400 | |. keep (Exactly 1) ((==) 'Z')
401 | )
402 | |> andThen (fromResult << (\_ -> Ok 0))
403 |
404 |
405 | optionalTZOffset : Parser Milliseconds
406 | optionalTZOffset =
407 | inContext "offset" <|
408 | ((succeed (,,)
409 | |= polarity
410 | |= digitsInRange "timezone hours" 2 0 23
411 | |. optional ':'
412 | |= digitsInRange "timezone minutes" 2 0 59
413 | )
414 | |> andThen (fromResult << getTZOffset)
415 | )
416 |
417 |
418 | polarity : Parser Int
419 | polarity =
420 | inContext "timezone polarity" <|
421 | (keep (Exactly 1)
422 | (\c ->
423 | c
424 | == '+'
425 | || c
426 | == '-'
427 | || c
428 | == '−'
429 | --U+2212
430 | )
431 | |> andThen
432 | (fromResult
433 | << -- Code has to do opposite of sign char
434 | (\sign ->
435 | if sign == "+" then
436 | Ok -1
437 | else
438 | Ok 1
439 | )
440 | )
441 | )
442 |
443 |
444 | getTZOffset : ( Int, Int, Int ) -> Result String Milliseconds
445 | getTZOffset ( polarity, hrs, min ) =
446 | Ok
447 | (polarity
448 | * hrs
449 | * hourMs
450 | + polarity
451 | * min
452 | * minuteMs
453 | )
454 |
--------------------------------------------------------------------------------
/src/Time/TimeZone.elm:
--------------------------------------------------------------------------------
1 | module Time.TimeZone
2 | exposing
3 | ( TimeZone
4 | , name
5 | , abbreviation
6 | , offset
7 | , offsetString
8 | , setName
9 | , unpack
10 | )
11 |
12 | {-| This module defines a representations for Timezone information.
13 |
14 |
15 | # TimeZone values
16 |
17 | @docs TimeZone, name, abbreviation, offset, offsetString
18 |
19 |
20 | # Manipulating TimeZones
21 |
22 | @docs setName
23 |
24 |
25 | # Constructing TimeZones
26 |
27 | @docs unpack
28 |
29 | -}
30 |
31 | import Char
32 | import Debug
33 | exposing
34 | ( log
35 | )
36 | import Parser
37 | exposing
38 | ( (|.)
39 | , (|=)
40 | , Count(..)
41 | , Error
42 | , Parser
43 | , andThen
44 | , delayedCommit
45 | , end
46 | , fail
47 | , ignore
48 | , inContext
49 | , keep
50 | , map
51 | , oneOf
52 | , oneOrMore
53 | , run
54 | , succeed
55 | , zeroOrMore
56 | )
57 | import Time exposing (Time)
58 | import Time.Internal exposing (..)
59 |
60 |
61 | {-| TimeZone represents the opaque type of timezone values. These are
62 | generally loaded from an external source via `unpack`.
63 |
64 | See also .
65 |
66 | -}
67 | type TimeZone
68 | = TimeZone
69 | { name : String
70 | , spans : List Span
71 | }
72 |
73 |
74 | {-| Spans represent variations within a TimeZone. A Time has an
75 | associated Span if `.from <= t < .until`.
76 |
77 | `offset` is the Span's UTC offset in milliseconds.
78 |
79 | -}
80 | type alias Span =
81 | { from : Time
82 | , until : Time
83 | , abbreviation : String
84 | , offset : Int
85 | }
86 |
87 |
88 | {-| Given an arbitrary Time and a TimeZone, abbreviation returns the
89 | TimeZone's abbreviation at that Time.
90 | -}
91 | abbreviation : Time -> TimeZone -> String
92 | abbreviation time (TimeZone { spans }) =
93 | find time spans |> .abbreviation
94 |
95 |
96 | {-| Given an arbitrary Time and a TimeZone, offset returns the
97 | TimeZone's UTC offset in milliseconds at that Time.
98 | -}
99 | offset : Time -> TimeZone -> Int
100 | offset time (TimeZone { spans }) =
101 | find time spans |> .offset
102 |
103 |
104 | {-| Given an arbitrary Time and TimeZone, offsetString returns an
105 | ISO8601-formatted UTC offset for at that Time.
106 | -}
107 | offsetString : Time -> TimeZone -> String
108 | offsetString time timeZone =
109 | let
110 | utcOffset =
111 | offset time timeZone // minuteMs
112 |
113 | hours =
114 | abs utcOffset // 60
115 |
116 | minutes =
117 | abs utcOffset % 60
118 |
119 | string =
120 | padded hours ++ ":" ++ padded minutes
121 | in
122 | if utcOffset <= 0 then
123 | "+" ++ string
124 | else
125 | "-" ++ string
126 |
127 |
128 | find : Time -> List Span -> Span
129 | find time spans =
130 | let
131 | go xs =
132 | case xs of
133 | [] ->
134 | Debug.crash "find: invalid span list"
135 |
136 | x :: xs ->
137 | if time >= x.from && time < x.until then
138 | x
139 | else
140 | go xs
141 | in
142 | go spans
143 |
144 |
145 | {-| setName updates a TimeZone's name.
146 | -}
147 | setName : String -> TimeZone -> TimeZone
148 | setName name (TimeZone tz) =
149 | TimeZone { tz | name = name }
150 |
151 |
152 | {-| name returns a TimeZone's name.
153 | -}
154 | name : TimeZone -> String
155 | name (TimeZone { name }) =
156 | name
157 |
158 |
159 | {-| unpackNew decodes a packed zone data object into a TimeZone value.
160 |
161 | See also
162 |
163 | -}
164 | unpack : String -> Result Error TimeZone
165 | unpack data =
166 | run packedTimeZone data
167 |
168 |
169 | {-| packedTimeZoneNew parses a zone data string into a TimeZone, validating that
170 | the data format invariants hold.
171 | -}
172 | packedTimeZone : Parser TimeZone
173 | packedTimeZone =
174 | let
175 | decode =
176 | (succeed PackedTimeZone
177 | |= parseName
178 | |. pipe
179 | |= parseAbbrevs
180 | |. pipe
181 | |= parseOffsets
182 | |. pipe
183 | |= parseIndices
184 | |. pipe
185 | |= parseDiffs
186 | )
187 |
188 | validate data =
189 | let
190 | abbrevs =
191 | List.length data.abbrevs
192 |
193 | offsets =
194 | List.length data.offsets
195 |
196 | maxIndex =
197 | List.maximum data.indices
198 | |> Maybe.withDefault 0
199 | in
200 | if abbrevs /= offsets then
201 | fail "abbrevs and offsets have different lengths"
202 | else if maxIndex >= abbrevs then
203 | fail "highest index is longer than both abbrevs and offsets"
204 | else
205 | succeed data
206 |
207 | span times data i idx =
208 | { from = times !! i
209 | , until = times !! (i + 1)
210 | , abbreviation = data.abbrevs !! idx
211 | , offset = round (data.offsets !! idx * minuteMs)
212 | }
213 |
214 | convert data =
215 | let
216 | times =
217 | if not <| List.isEmpty data.diffs then
218 | List.scanl (+) (data.diffs !! 0) (List.drop 1 data.diffs)
219 | else
220 | []
221 |
222 | -- surround times with - and +infinity
223 | paddedTimes =
224 | [ -1 / 0 ] ++ times ++ [ 1 / 0 ]
225 | in
226 | TimeZone
227 | { name = data.name
228 | , spans = List.indexedMap (span paddedTimes data) data.indices
229 | }
230 | in
231 | decode
232 | |> andThen validate
233 | |> map convert
234 |
235 |
236 | type alias PackedTimeZone =
237 | { name : String
238 | , abbrevs : List String
239 | , offsets : List Float
240 | , indices : List Int
241 | , diffs : List Float
242 | }
243 |
244 |
245 | {-| Parse the name of the timezone
246 | -}
247 | parseName : Parser String
248 | parseName =
249 | inContext "name" <|
250 | succeed identity
251 | |= keep oneOrMore ((/=) '|')
252 |
253 |
254 | {-| Parse the first abbrev and then use `abbrevsHelp` to find
255 | the remaining ones.
256 | -}
257 | parseAbbrevs : Parser (List String)
258 | parseAbbrevs =
259 | let
260 | abbrev : Parser String
261 | abbrev =
262 | keep oneOrMore (\c -> c /= ' ' && c /= '|')
263 |
264 | helper : List String -> Parser (List String)
265 | helper revTerms =
266 | oneOf
267 | [ next
268 | |> andThen (\s -> helper (s :: revTerms))
269 | , succeed (List.reverse revTerms)
270 | ]
271 |
272 | next : Parser String
273 | next =
274 | succeed identity
275 | |. parseSpace
276 | |= abbrev
277 | in
278 | inContext "abbrevs" <|
279 | succeed identity
280 | |= andThen (\s -> helper [ s ]) abbrev
281 |
282 |
283 | parseOffsets : Parser (List Float)
284 | parseOffsets =
285 | let
286 | offset : Parser Float
287 | offset =
288 | (succeed (,,)
289 | |= parseSign
290 | |= parseWhole
291 | |= parseFrac
292 | )
293 | |> andThen convertBase60
294 |
295 | convertBase60 : ( Int, String, String ) -> Parser Float
296 | convertBase60 ( sign, whole, frac ) =
297 | if whole == "" && frac == "" then
298 | fail "expected an alphanumeric character or ."
299 | else
300 | succeed <| unsafeBase60 sign whole frac
301 |
302 | convertFrac : String -> Parser String
303 | convertFrac frac =
304 | succeed frac
305 |
306 | helper : List Float -> Parser (List Float)
307 | helper revTerms =
308 | oneOf
309 | [ next
310 | |> andThen (\f -> helper (f :: revTerms))
311 | , succeed (List.reverse revTerms)
312 | ]
313 |
314 | next : Parser Float
315 | next =
316 | succeed identity
317 | |. parseSpace
318 | |= offset
319 | in
320 | inContext "offsets" <|
321 | succeed identity
322 | |= andThen (\f -> helper [ f ]) offset
323 |
324 |
325 | parseIndices : Parser (List Int)
326 | parseIndices =
327 | let
328 | helper : List Int -> Parser (List Int)
329 | helper revTerms =
330 | oneOf
331 | [ next
332 | |> andThen (\i -> helper (i :: revTerms))
333 | , succeed (List.reverse revTerms)
334 | ]
335 |
336 | next : Parser Int
337 | next =
338 | succeed identity
339 | |= index
340 |
341 | index : Parser Int
342 | index =
343 | keep (Exactly 1) (\c -> Char.isDigit c)
344 | |> andThen convertDecimal
345 |
346 | convertDecimal : String -> Parser Int
347 | convertDecimal digit =
348 | case String.toInt digit of
349 | Err msg ->
350 | fail msg
351 |
352 | Ok value ->
353 | succeed value
354 | in
355 | inContext "indices" <|
356 | succeed identity
357 | |= andThen (\i -> helper [ i ]) index
358 |
359 |
360 | parseDiffs : Parser (List Float)
361 | parseDiffs =
362 | let
363 | emptyDiffs : Parser (List Float)
364 | emptyDiffs =
365 | (succeed identity
366 | |. pipe
367 | )
368 | |> andThen (\_ -> succeed [])
369 |
370 | diffsEnd : Parser (List Float)
371 | diffsEnd =
372 | (succeed identity
373 | |. end
374 | )
375 | |> andThen (\_ -> succeed [])
376 |
377 | helper : List Float -> Parser (List Float)
378 | helper revTerms =
379 | oneOf
380 | [ next
381 | |> andThen (\f -> helper (f :: revTerms))
382 | , succeed (List.reverse revTerms)
383 | ]
384 |
385 | next : Parser Float
386 | next =
387 | succeed identity
388 | |. parseSpace
389 | |= diff
390 |
391 | diff : Parser Float
392 | diff =
393 | (succeed (,,)
394 | |= parseSign
395 | |= parseWhole
396 | |= parseFrac
397 | )
398 | |> andThen convertBase60Times60000
399 |
400 | convertBase60Times60000 : ( Int, String, String ) -> Parser Float
401 | convertBase60Times60000 ( sign, whole, frac ) =
402 | if whole == "" && frac == "" then
403 | fail "expected an alphanumeric character or ."
404 | else
405 | succeed <| (*) 60000 (unsafeBase60 sign whole frac)
406 | in
407 | inContext "diffs" <|
408 | oneOf
409 | [ emptyDiffs
410 | , diffsEnd
411 | , andThen (\f -> helper [ f ]) diff
412 | ]
413 |
414 |
415 | pipe : Parser ()
416 | pipe =
417 | ignore (Exactly 1) ((==) '|')
418 |
419 |
420 | parseSpace : Parser ()
421 | parseSpace =
422 | ignore (Exactly 1) ((==) ' ')
423 |
424 |
425 | parseSign : Parser Int
426 | parseSign =
427 | let
428 | minusOne : String -> Parser Int
429 | minusOne hyphen =
430 | succeed -1
431 | in
432 | oneOf
433 | [ keep (Exactly 1) (\c -> c == '-')
434 | |> andThen minusOne
435 | , succeed 1
436 | ]
437 |
438 |
439 | parseWhole : Parser String
440 | parseWhole =
441 | keep zeroOrMore (\c -> unsafeBase60Digit c)
442 |
443 |
444 | parseFrac : Parser String
445 | parseFrac =
446 | oneOf
447 | [ parseSuccessfulFrac
448 | , succeed ""
449 | ]
450 |
451 |
452 | unsafeBase60Digit : Char -> Bool
453 | unsafeBase60Digit c =
454 | Char.isDigit c || Char.isUpper c || Char.isLower c
455 |
456 |
457 | parseSuccessfulFrac : Parser String
458 | parseSuccessfulFrac =
459 | (succeed identity
460 | |. ignore (Exactly 1) (\c -> c == '.')
461 | |= keep oneOrMore (\c -> unsafeBase60Digit c)
462 | )
463 |
464 |
465 | unsafeBase60 : Int -> String -> String -> Float
466 | unsafeBase60 sign whole frac =
467 | let
468 | toNum c =
469 | let
470 | n =
471 | Char.toCode c |> toFloat
472 | in
473 | if n > 96 then
474 | n - 87
475 | else if n > 64 then
476 | n - 29
477 | else
478 | n - 48
479 |
480 | toWhole cs acc =
481 | case cs of
482 | [] ->
483 | acc
484 |
485 | c :: cs ->
486 | toWhole cs (60 * acc + toNum c)
487 |
488 | toFrac cs mul acc =
489 | let
490 | mul_ =
491 | mul / 60
492 | in
493 | case cs of
494 | [] ->
495 | acc
496 |
497 | c :: cs ->
498 | toFrac cs mul_ (acc + mul_ * toNum c)
499 | in
500 | toWhole (String.toList whole) 0
501 | |> toFrac (String.toList frac) 1
502 | |> ((*) (toFloat sign))
503 |
504 |
505 | (!!) : List a -> Int -> a
506 | (!!) xs i =
507 | case List.head (List.drop i xs) of
508 | Nothing ->
509 | Debug.crash ("index too large: xs=" ++ toString xs ++ " i=" ++ toString i)
510 |
511 | Just x ->
512 | x
513 |
--------------------------------------------------------------------------------
/tests/TestIso8601.elm:
--------------------------------------------------------------------------------
1 | module TestIso8601 exposing (..)
2 |
3 | import Expect exposing (Expectation)
4 | import Fuzz exposing (Fuzzer, constant, int, intRange, oneOf)
5 | import Test exposing (..)
6 | import TestDate exposing (someDate)
7 | import Time.Date exposing (Date, date)
8 | import Time.DateTime as DT
9 | exposing
10 | ( addHours
11 | , addMinutes
12 | , dateTime
13 | , epoch
14 | , millisecond
15 | , setMillisecond
16 | , zero
17 | )
18 | import Time.Iso8601 as Iso8601
19 | exposing
20 | ( fromDate
21 | , fromDateTime
22 | , fromZonedDateTime
23 | , toDate
24 | , toDateTime
25 | )
26 | import Time.TimeZones exposing (europe_bucharest)
27 | import Time.ZonedDateTime as ZonedDateTime
28 | exposing
29 | ( zonedDateTime
30 | )
31 |
32 |
33 | {-| -}
34 | toFromISO8601 : Test
35 | toFromISO8601 =
36 | let
37 | parseEq input expectedDateTime =
38 | case toDateTime input of
39 | Err error ->
40 | toString error
41 | |> Expect.fail
42 |
43 | Ok actualDateTime ->
44 | if actualDateTime == expectedDateTime then
45 | Expect.pass
46 | else
47 | Expect.fail ("expected '" ++ Iso8601.fromDateTime actualDateTime ++ "' to equal '" ++ Iso8601.fromDateTime expectedDateTime ++ "' from input '" ++ input ++ "'")
48 |
49 | parseEqDate input expectedDate =
50 | case toDate input of
51 | Err error ->
52 | toString error
53 | |> Expect.fail
54 |
55 | Ok actualDate ->
56 | if actualDate == expectedDate then
57 | Expect.pass
58 | else
59 | Expect.fail ("expected '" ++ Iso8601.fromDate actualDate ++ "' to equal '" ++ Iso8601.fromDate expectedDate ++ "' from input '" ++ input ++ "'")
60 |
61 | parseFails input =
62 | case toDateTime input of
63 | Err _ ->
64 | Expect.pass
65 |
66 | Ok _ ->
67 | Expect.fail ("parsing '" ++ input ++ "' should have failed")
68 |
69 | parseMs input ms =
70 | case toDateTime input of
71 | Err _ ->
72 | Expect.fail "parse failed"
73 |
74 | Ok dt ->
75 | Expect.equal (millisecond dt) ms
76 | in
77 | describe "Time.DateTime.{to,from}ISO8601"
78 | [ test "toISO8601 of epoch is correct" <|
79 | \() ->
80 | Iso8601.fromDateTime epoch
81 | |> Expect.equal "1970-01-01T00:00:00.000Z"
82 | , test "fromISO8601 is the inverse of toISO8601" <|
83 | \() ->
84 | Iso8601.fromDateTime epoch
85 | |> flip parseEq epoch
86 | , test "fromISO8601 fails to parse invalid strings" <|
87 | \() -> parseFails "foo"
88 | , test "fromISO8601 fails to parse invalid UTC datetimes" <|
89 | \() -> parseFails "1993-02-29T00:00:00Z"
90 | , test "fromISO8601 can parse valid UTC datetime strings" <|
91 | \() -> parseEq "1992-05-29T12:25:12Z" (dateTime { zero | year = 1992, month = 5, day = 29, hour = 12, minute = 25, second = 12 })
92 | , test "fromISO8601 can parse valid UTC datetime strings including milliseconds" <|
93 | \() -> parseEq "1992-05-29T12:25:12.001Z" (dateTime { zero | year = 1992, month = 5, day = 29, hour = 12, minute = 25, second = 12, millisecond = 1 })
94 | , test "fromISO8601 can parse valid UTC datetime strings including zero milliseconds" <|
95 | \() -> parseEq "1992-05-29T12:25:12.000Z" (dateTime { zero | year = 1992, month = 5, day = 29, hour = 12, minute = 25, second = 12, millisecond = 0 })
96 | , test "fromISO8601 can parse valid UTC datetime strings including milliseconds and offset" <|
97 | \() -> parseEq "1992-05-29T12:25:12.001-04:00" (dateTime { zero | year = 1992, month = 5, day = 29, hour = 16, minute = 25, second = 12, millisecond = 1 })
98 | , test "fromISO8601 can parse valid UTC datetime strings including zero milliseconds and offset" <|
99 | \() -> parseEq "1992-05-29T12:25:12.000-04:00" (dateTime { zero | year = 1992, month = 5, day = 29, hour = 16, minute = 25, second = 12, millisecond = 0 })
100 | , test "fromISO8601 can parse valid fractions in the hundreds" <|
101 | \() -> parseMs "2016-11-14T03:56:12.123Z" 123
102 | , test "fromISO8601 can parse valid fractions in the tens" <|
103 | \() -> parseMs "2016-11-14T03:56:12.12Z" 120
104 | , test "fromISO8601 can parse valid fractions in the ones" <|
105 | \() -> parseMs "2016-11-14T03:56:12.1Z" 100
106 | , test "fromISO8601 can parse valid padded fractions in the tens" <|
107 | \() -> parseMs "2016-11-14T03:56:12.01Z" 10
108 | , test "fromISO8601 can parse valid padded fractions in the ones" <|
109 | \() -> parseMs "2016-11-14T03:56:12.001Z" 1
110 | , test "fromISO8601 can parse valid padded fractions in the zeros" <|
111 | \() -> parseMs "2016-11-14T03:56:12.000Z" 0
112 | , test "fromISO8601 fractions are capped at millisecond precision" <|
113 | \() -> parseMs "2016-11-14T03:56:12.12345Z" 123
114 | , test "fromISO8601 fractions are capped at millisecond precision with padding" <|
115 | \() -> parseMs "2016-11-14T03:56:12.0012345Z" 1
116 | , test "fromISO8601 fractions are capped at millisecond precision with padding 2" <|
117 | \() -> parseMs "2016-11-14T03:56:12.0001234Z" 0
118 | , test "fromISO8601 fractions can be all zeros" <|
119 | \() -> parseMs "2016-11-14T03:56:12.000Z" 0
120 | , test "fromISO8601 zero fractions with offsets" <|
121 | \() -> parseMs "2017-07-03T11:27:11.000-0400" 0
122 | , test "fromISO8601 fraction 001 with offsets" <|
123 | \() -> parseMs "2017-07-03T11:27:11.001-0400" 1
124 | , test "fromISO8601 of a valid year before 1000 (with dashes) is correct" <|
125 | \() -> parseEqDate "0301-10-02" (date 301 10 2)
126 | , test "fromISO8601 of a valid year before 1000 (without dashes) is correct" <|
127 | \() -> parseEqDate "00011002" (date 1 10 2)
128 | , test "toISO8601 of a valid year before 1000 is correct" <|
129 | \() ->
130 | Iso8601.fromDate (date 301 10 2)
131 | |> Expect.equal "0301-10-02"
132 | , test "toISO8601 should format 3-digit milliseconds" <|
133 | \() ->
134 | epoch
135 | |> setMillisecond 396
136 | |> Iso8601.fromDateTime
137 | |> Expect.equal "1970-01-01T00:00:00.396Z"
138 | , test "toISO8601 should format 2-digit milliseconds" <|
139 | \() ->
140 | epoch
141 | |> setMillisecond 96
142 | |> Iso8601.fromDateTime
143 | |> Expect.equal "1970-01-01T00:00:00.096Z"
144 | , test "toISO8601 should format 1-digit milliseconds" <|
145 | \() ->
146 | epoch
147 | |> setMillisecond 6
148 | |> Iso8601.fromDateTime
149 | |> Expect.equal "1970-01-01T00:00:00.006Z"
150 | , fuzz4 (intRange -23 23) (intRange 0 59) (oneOf [ constant "-", constant "−" ]) (oneOf [ constant ":", constant "" ]) "fromISO8601 parses offsets correctly" <|
151 | \hour minute negStr separator ->
152 | let
153 | ( sign, signStr ) =
154 | if hour <= 0 then
155 | ( -1, "+" )
156 | else
157 | ( 1, negStr )
158 |
159 | padded n =
160 | if abs n < 10 then
161 | "0" ++ toString (abs n)
162 | else
163 | toString (abs n)
164 |
165 | input =
166 | "1992-05-29T12:25:12" ++ signStr ++ padded hour ++ separator ++ padded minute
167 |
168 | output =
169 | dateTime { zero | year = 1992, month = 5, day = 29, hour = 12, minute = 25, second = 12 }
170 | |> addHours hour
171 | |> addMinutes (sign * minute)
172 | in
173 | parseEq input output
174 | , fuzz2 (oneOf [ constant "-", constant "" ]) (oneOf [ constant ":", constant "" ]) "fromISO8601 handles basic and extended time/date formats correctly" <|
175 | \dateSep timeSep ->
176 | let
177 | input =
178 | String.join dateSep [ "1992", "05", "29" ] ++ "T" ++ String.join timeSep [ "12", "25", "12" ] ++ "Z"
179 |
180 | output =
181 | dateTime { zero | year = 1992, month = 5, day = 29, hour = 12, minute = 25, second = 12 }
182 | in
183 | parseEq input output
184 | ]
185 |
186 |
187 | toFromDateISO8601 : Test
188 | toFromDateISO8601 =
189 | let
190 | renderEq : Date -> String -> () -> Expectation
191 | renderEq date output () =
192 | fromDate date
193 | |> Expect.equal output
194 |
195 | parseEq input date () =
196 | case ( toDate input, date ) of
197 | ( Err message, _ ) ->
198 | Expect.fail (toString message ++ " in input '" ++ input ++ "'")
199 |
200 | ( Ok date1, date2 ) ->
201 | if date1 == date2 then
202 | Expect.pass
203 | else
204 | Expect.fail ("expected '" ++ fromDate date1 ++ "' to equal '" ++ fromDate date2 ++ "' from input '" ++ input ++ "'")
205 |
206 | parseFails input () =
207 | case toDateTime input of
208 | Err _ ->
209 | Expect.pass
210 |
211 | Ok _ ->
212 | Expect.fail ("parsing '" ++ input ++ "' should have failed")
213 | in
214 | describe "Time.Date.{to,from}ISO8601"
215 | [ test "toISO8601 of epoch is correct" <|
216 | renderEq (date 1970 1 1) "1970-01-01"
217 | , test "toISO8601 of some date is correct" <|
218 | renderEq someDate "1992-05-29"
219 | , test "fromISO8601 of a valid date is correct" <|
220 | parseEq "1992-12-29" (date 1992 12 29)
221 | , test "fromISO8601 of a valid padded date is correct" <|
222 | parseEq "1992-05-29" (date 1992 5 29)
223 | , test "fromISO8601 of a valid padded date with no delimeters is correct" <|
224 | parseEq "19920529" (date 1992 5 29)
225 | , test "fromISO8601 of a badly-formatted date fails" <|
226 | parseFails ""
227 | , test "fromISO8601 of a badly-formatted date fails 2" <|
228 | parseFails "1992-05"
229 | , test "fromISO8601 of an invalid date fails" <|
230 | parseFails "1991-02-31"
231 | ]
232 |
233 |
234 | summerTimes : Test
235 | summerTimes =
236 | describe "Time.DateTime.{zonedDateTime,fromDateTime}"
237 | [ test "Zoned from DateTime applies correct offset before the end of summer time" <|
238 | \() ->
239 | DT.dateTime { zero | year = 2016, month = 10, day = 29, hour = 23, minute = 59 }
240 | |> ZonedDateTime.fromDateTime (europe_bucharest ())
241 | |> fromZonedDateTime
242 | |> Expect.equal "2016-10-30T02:59:00.000+03:00"
243 | , test "Zoned from DateTime applies correct offset during the end of summer time" <|
244 | \() ->
245 | DT.dateTime { zero | year = 2016, month = 10, day = 30, hour = 0 }
246 | |> ZonedDateTime.fromDateTime (europe_bucharest ())
247 | |> fromZonedDateTime
248 | |> Expect.equal "2016-10-30T03:00:00.000+02:00"
249 | , test "Zoned from DateTime applies correct offset after the end of summer time" <|
250 | \() ->
251 | DT.dateTime { zero | year = 2016, month = 10, day = 30, hour = 1 }
252 | |> ZonedDateTime.fromDateTime (europe_bucharest ())
253 | |> fromZonedDateTime
254 | |> Expect.equal "2016-10-30T03:00:00.000+02:00"
255 | , test "Zoned constructor applies correct offset before the end of summer time" <|
256 | \() ->
257 | zonedDateTime (europe_bucharest ()) { zero | year = 2016, month = 10, day = 30, hour = 2 }
258 | |> fromZonedDateTime
259 | |> Expect.equal "2016-10-30T02:00:00.000+03:00"
260 | , test "Zoned constructor applies correct offset during the end of summer time" <|
261 | \() ->
262 | zonedDateTime (europe_bucharest ()) { zero | year = 2016, month = 10, day = 30, hour = 3 }
263 | |> fromZonedDateTime
264 | |> Expect.equal "2016-10-30T03:00:00.000+02:00"
265 | , test "Zoned constructor applies correct offset after the end of summer time" <|
266 | \() ->
267 | zonedDateTime (europe_bucharest ()) { zero | year = 2016, month = 10, day = 30, hour = 4 }
268 | |> fromZonedDateTime
269 | |> Expect.equal "2016-10-30T04:00:00.000+02:00"
270 | , test "toISO8601 should format 3-digit milliseconds" <|
271 | \() ->
272 | zonedDateTime (europe_bucharest ()) { zero | year = 2017, millisecond = 396 }
273 | |> fromZonedDateTime
274 | |> Expect.equal "2017-01-01T00:00:00.396+02:00"
275 | , test "toISO8601 should format 2-digit milliseconds" <|
276 | \() ->
277 | zonedDateTime (europe_bucharest ()) { zero | year = 2017, millisecond = 96 }
278 | |> fromZonedDateTime
279 | |> Expect.equal "2017-01-01T00:00:00.096+02:00"
280 | , test "toISO8601 should format 1-digit milliseconds" <|
281 | \() ->
282 | zonedDateTime (europe_bucharest ()) { zero | year = 2017, millisecond = 6 }
283 | |> fromZonedDateTime
284 | |> Expect.equal "2017-01-01T00:00:00.006+02:00"
285 | ]
286 |
--------------------------------------------------------------------------------
/src/Time/Date.elm:
--------------------------------------------------------------------------------
1 | module Time.Date
2 | exposing
3 | ( Date
4 | , DateDelta
5 | , Weekday(..)
6 | , addDays
7 | , addMonths
8 | , addYears
9 | , compare
10 | , date
11 | , day
12 | , daysInMonth
13 | , delta
14 | , fromTuple
15 | , isLeapYear
16 | , isValidDate
17 | , month
18 | , setDay
19 | , setMonth
20 | , setYear
21 | , toTuple
22 | , weekday
23 | , year
24 | )
25 |
26 | {-| This module defines a timezone-independent Date type which can
27 | represent any date of the proleptic Gregorian calendar.
28 |
29 |
30 | # Dates
31 |
32 | @docs Date
33 |
34 |
35 | # Constructing Dates
36 |
37 | @docs date, fromTuple, toTuple
38 |
39 |
40 | # Inspecting Dates
41 |
42 | @docs year, month, day, Weekday, weekday
43 |
44 |
45 | # Manipulating Dates
46 |
47 | @docs setYear, setMonth, setDay, addYears, addMonths, addDays
48 |
49 |
50 | # Comparing Dates
51 |
52 | @docs compare, DateDelta, delta
53 |
54 |
55 | # Helper functions
56 |
57 | @docs isValidDate, isLeapYear, daysInMonth
58 |
59 | -}
60 |
61 |
62 | {-| Date is the opaque type for all Date values. Values of this type
63 | are guaranteed to represent valid proleptic Gregorian calendar dates.
64 | -}
65 | type Date
66 | = Date
67 | { year : Int
68 | , month : Int
69 | , day : Int
70 | }
71 |
72 |
73 | {-| Data type used to represent the days of the week.
74 | -}
75 | type Weekday
76 | = Mon
77 | | Tue
78 | | Wed
79 | | Thu
80 | | Fri
81 | | Sat
82 | | Sun
83 |
84 |
85 | {-| DateDelta represents a delta between two dates.
86 | -}
87 | type alias DateDelta =
88 | { years : Int
89 | , months : Int
90 | , days : Int
91 | }
92 |
93 |
94 | {-| date constructs a Date value given a year, a month and a day.
95 | Invalid values are clamped to the nearest valid date.
96 |
97 | d : Date
98 | d =
99 | date 2018 5 29
100 |
101 | year d --> 2018
102 | month d --> 5
103 | day d --> 29
104 |
105 | -}
106 | date : Int -> Int -> Int -> Date
107 | date year month day =
108 | firstValid year (clampMonth month) (clampDay day)
109 |
110 |
111 | {-| year returns a Date's year as an Int.
112 |
113 | year (date 2018 5 26)
114 | --> 2018
115 |
116 | -}
117 | year : Date -> Int
118 | year (Date { year }) =
119 | year
120 |
121 |
122 | {-| month returns a Date's month as an Int. Guaranteed to be in the
123 | range [1, 12].
124 |
125 | month (date 2018 13 26) -- Note month will be clamped
126 | --> 12
127 |
128 | -}
129 | month : Date -> Int
130 | month (Date { month }) =
131 | month
132 |
133 |
134 | {-| day returns a Date's year as an Int. Guaranteed to be valid for
135 | the Date's (year, month) pair and in the range [1, 31].
136 |
137 | day (date 2018 2 28)
138 | --> 28
139 |
140 | day (date 2018 2 29)
141 | --> 28 -- observed clamped
142 |
143 | day (date 2000 2 29)
144 | --> 29 -- leap year
145 |
146 | -}
147 | day : Date -> Int
148 | day (Date { day }) =
149 | day
150 |
151 |
152 | {-| weekday returns the day of week for a given Date.
153 |
154 | This uses Sakamoto's method to determine the day of week.
155 |
156 | weekday (date 2018 5 26)
157 | --> Sat
158 |
159 | -}
160 | weekday : Date -> Weekday
161 | weekday (Date { year, month, day }) =
162 | let
163 | m =
164 | if month == 1 then
165 | 0
166 | else if month == 2 then
167 | 3
168 | else if month == 3 then
169 | 2
170 | else if month == 4 then
171 | 5
172 | else if month == 5 then
173 | 0
174 | else if month == 6 then
175 | 3
176 | else if month == 7 then
177 | 5
178 | else if month == 8 then
179 | 1
180 | else if month == 9 then
181 | 4
182 | else if month == 10 then
183 | 6
184 | else if month == 11 then
185 | 2
186 | else
187 | 4
188 |
189 | y =
190 | if month < 3 then
191 | year - 1
192 | else
193 | year
194 |
195 | d =
196 | (y + y // 4 - y // 100 + y // 400 + m + day) % 7
197 | in
198 | if d == 0 then
199 | Sun
200 | else if d == 1 then
201 | Mon
202 | else if d == 2 then
203 | Tue
204 | else if d == 3 then
205 | Wed
206 | else if d == 4 then
207 | Thu
208 | else if d == 5 then
209 | Fri
210 | else
211 | Sat
212 |
213 |
214 | {-| setYear updates a Date's year. Invalid values are clamped to the
215 | nearest valid date.
216 |
217 | date 2000 5 26
218 | |> setYear 2016
219 | |> year
220 | --> 2016
221 |
222 | -}
223 | setYear : Int -> Date -> Date
224 | setYear year (Date ({ month, day } as date)) =
225 | firstValid year month day
226 |
227 |
228 | {-| setMonth updates a Date's month. Invalid values are clamped to the
229 | nearest valid date.
230 |
231 | date 2016 5 26
232 | |> setMonth 6
233 | |> month
234 | --> 6
235 |
236 | date 2016 5 26
237 | |> setMonth 13 -- will be clamped
238 | |> month
239 | --> 12
240 |
241 | -}
242 | setMonth : Int -> Date -> Date
243 | setMonth month (Date ({ year, day } as date)) =
244 | firstValid year (clampMonth month) day
245 |
246 |
247 | {-| setDay updates a Date's day. Invalid values are clamped to the
248 | nearest valid date.
249 |
250 | date 2016 2 26
251 | |> setDay 28
252 | |> day
253 | --> 28
254 |
255 | date 2016 2 28
256 | |> setDay 29 -- leap year
257 | |> day
258 | --> 29
259 |
260 | date 2015 2 28
261 | |> setDay 29 -- clamped
262 | |> day
263 | --> 28
264 |
265 | -}
266 | setDay : Int -> Date -> Date
267 | setDay day (Date ({ year, month } as date)) =
268 | firstValid year month (clampDay day)
269 |
270 |
271 | {-| addYears adds a relative number (positive or negative) of years to
272 | a Date, ensuring that the return value represents a valid Date. If
273 | the new date is not valid, days are subtracted from it until a valid
274 | Date can be produced.
275 |
276 | date 2000 2 29
277 | |> addYears -1 -- will no longer be leap year
278 | |> day
279 | --> 28
280 |
281 | -}
282 | addYears : Int -> Date -> Date
283 | addYears years (Date ({ year, month, day } as date)) =
284 | firstValid (year + years) month day
285 |
286 |
287 | {-| addMonths adds a relative number (positive or negative) of months to
288 | a Date, ensuring that the return value represents a valid Date. Its
289 | semantics are the same as `addYears`.
290 |
291 | date 2018 3 31
292 | |> addMonths -1 -- Switch to Feb
293 | |> day
294 | --> 28
295 |
296 | -}
297 | addMonths : Int -> Date -> Date
298 | addMonths months (Date { year, month, day }) =
299 | let
300 | ms =
301 | year * 12 + month - 1 + months
302 |
303 | yo =
304 | if ms < 0 then
305 | -1
306 | else
307 | 0
308 | in
309 | date (((ms - yo) // 12) + yo) ((ms % 12) + 1) day
310 |
311 |
312 | {-| days adds an exact number (positive or negative) of days to a
313 | Date. Adding or subtracting days always produces a valid Date so
314 | there is no fuzzing logic here like there is in `add{Months,Years}`.
315 |
316 | date 2018 2 28
317 | |> addDays 1
318 | |> month
319 | --> 3 -- March
320 |
321 | -}
322 | addDays : Int -> Date -> Date
323 | addDays days (Date ({ year, month, day } as date)) =
324 | daysFromYearMonthDay year month day
325 | |> (+) days
326 | |> dateFromDays
327 |
328 |
329 | {-| compare two Dates.
330 |
331 | Note: since this conflicts with **Basics.compare**, have to
332 | preface with **Time.Date.**; see this example:
333 |
334 | date 2018 1 28
335 | |> addYears -1
336 | |> addMonths 1
337 | |> Time.Date.compare (date 2017 2 29)
338 | --> EQ
339 |
340 | -}
341 | compare : Date -> Date -> Order
342 | compare d1 d2 =
343 | Basics.compare (toTuple d1) (toTuple d2)
344 |
345 |
346 | {-| delta returns the relative number of years, months and days between two Dates.
347 |
348 | Each field is accumulative by itself. That is, `days` not only shows the
349 | difference caused by the two `day` entries in the two `Date` arguments, but
350 | also the added days caused by differences in `months` and `years`. For `months`
351 | and `years`, is the count across month and year change boundaries respectively; illustrated
352 | by last example below.
353 |
354 | -- 3 examples showing that, if the `year` and `month`
355 | -- are the same in the two `Date` values, then the
356 | -- `years` and `months` result values remain constant
357 | -- in spite of large differences in the two inputs'
358 | -- `day` setting:
359 |
360 | delta (date 2019 1 1) (date 2018 1 1)
361 | --> { years = 1
362 | --> , months = 12
363 | --> , days = 365
364 | --> }
365 |
366 | delta (date 2019 1 31) (date 2018 1 1)
367 | --> { years = 1
368 | --> , months = 12
369 | --> , days = 395
370 | --> }
371 |
372 | delta (date 2019 1 1) (date 2018 1 31)
373 | --> { years = 1
374 | --> , months = 12
375 | --> , days = 335
376 | --> }
377 |
378 | -- 1 day apart but from last day of year to first
379 | -- day of next year:
380 |
381 | delta (date 2019 1 1) (date 2018 12 31)
382 | --> { years = 1
383 | --> , months = 1
384 | --> , days = 1
385 | --> }
386 |
387 | -}
388 | delta : Date -> Date -> DateDelta
389 | delta (Date d1) (Date d2) =
390 | { years = d1.year - d2.year
391 | , months = (abs d1.year * 12 + d1.month) - (abs d2.year * 12 + d2.month)
392 | , days =
393 | daysFromYearMonthDay d1.year d1.month d1.day
394 | - daysFromYearMonthDay d2.year d2.month d2.day
395 | }
396 |
397 |
398 | {-| toTuple converts a Date value into a (year, month, day) tuple.
399 | This is useful if you want to use Dates as Dict keys.
400 |
401 | date 2018 5 26
402 | |> toTuple
403 | --> (2018, 5, 26)
404 |
405 | -}
406 | toTuple : Date -> ( Int, Int, Int )
407 | toTuple (Date { year, month, day }) =
408 | ( year, month, day )
409 |
410 |
411 | {-| fromTuple converts a (year, month, day) tuple into a Date value.
412 |
413 | (2018, 5, 26)
414 | |> fromTuple
415 | --> date 2018 5 26
416 |
417 | -}
418 | fromTuple : ( Int, Int, Int ) -> Date
419 | fromTuple ( year, month, day ) =
420 | date year month day
421 |
422 |
423 | {-| isValidDate returns True if the given year, month and day
424 | represent a valid date.
425 |
426 | NOTE: when you create a Date using `date`, it does not validate
427 | the `year`, `month`, or `day` used; rather it just clamps out-of-range
428 | values to "legal" values without notifying you. If you are worried
429 | about complete validation, pass the 3 values to this
430 | method first and it will validate it. This gives you a chance to
431 | abort creating a "bad" `Date`.
432 |
433 | isValidDate 2016 12 31
434 | --> True
435 |
436 | isValidDate 2016 12 32
437 | --> False
438 |
439 | isValidDate 2016 2 29 -- leap year
440 | --> True
441 |
442 | isValidDate 2018 2 29 -- not leap year
443 | --> False
444 |
445 | -}
446 | isValidDate : Int -> Int -> Int -> Bool
447 | isValidDate year month day =
448 | daysInMonth year month
449 | |> Maybe.map (\days -> day >= 1 && day <= days)
450 | |> Maybe.withDefault False
451 |
452 |
453 | {-| isLeapYear returns True if the given year is a leap year. The
454 | rules for leap years are as follows:
455 |
456 | - A year that is a multiple of 400 is a leap year.
457 |
458 | - A year that is a multiple of 100 but not of 400 is not a leap year.
459 |
460 | - A year that is a multiple of 4 but not of 100 is a leap year.
461 |
462 | isLeapYear 2016
463 | --> True
464 |
465 | isLeapYear 2018
466 | --> False
467 |
468 | isLeapYear 400
469 | --> True
470 |
471 | isLeapYear 500
472 | --> False
473 |
474 | isLeapYear (500 + 4)
475 | --> True
476 |
477 | -}
478 | isLeapYear : Int -> Bool
479 | isLeapYear y =
480 | y % 400 == 0 || y % 100 /= 0 && y % 4 == 0
481 |
482 |
483 | {-| daysInMonth returns the number of days in a month given a specific
484 | year, taking leap years into account.
485 |
486 | - A regular year has 365 days and the corresponding February has 28 days.
487 |
488 | - A leap year has 366 days and the corresponding February has 29 days.
489 |
490 | daysInMonth 2016 2
491 | --> Just 29
492 |
493 | daysInMonth 2018 2
494 | --> Just 28
495 |
496 | daysInMonth 2018 13 -- month out of range
497 | --> Nothing
498 |
499 | -}
500 | daysInMonth : Int -> Int -> Maybe Int
501 | daysInMonth y m =
502 | if m >= 1 && m <= 12 then
503 | Just <| unsafeDaysInMonth y m
504 | else
505 | Nothing
506 |
507 |
508 | unsafeDaysInMonth : Int -> Int -> Int
509 | unsafeDaysInMonth y m =
510 | if m == 1 then
511 | 31
512 | else if m == 2 && isLeapYear y then
513 | 29
514 | else if m == 2 then
515 | 28
516 | else if m == 3 then
517 | 31
518 | else if m == 4 then
519 | 30
520 | else if m == 5 then
521 | 31
522 | else if m == 6 then
523 | 30
524 | else if m == 7 then
525 | 31
526 | else if m == 8 then
527 | 31
528 | else if m == 9 then
529 | 30
530 | else if m == 10 then
531 | 31
532 | else if m == 11 then
533 | 30
534 | else if m == 12 then
535 | 31
536 | else
537 | Debug.crash <| "invalid call to unsafeDaysInMonth: year=" ++ toString y ++ " month=" ++ toString m
538 |
539 |
540 | firstValid : Int -> Int -> Int -> Date
541 | firstValid year month day =
542 | let
543 | ( y, m, d ) =
544 | if isValidDate year month day then
545 | ( year, month, day )
546 | else if isValidDate year month (day - 1) then
547 | ( year, month, day - 1 )
548 | else if isValidDate year month (day - 2) then
549 | ( year, month, day - 2 )
550 | else
551 | ( year, month, day - 3 )
552 | in
553 | Date { year = y, month = m, day = d }
554 |
555 |
556 | daysFromYearMonthDay : Int -> Int -> Int -> Int
557 | daysFromYearMonthDay year month day =
558 | let
559 | yds =
560 | daysFromYear year
561 |
562 | mds =
563 | daysFromYearMonth year month
564 |
565 | dds =
566 | day - 1
567 | in
568 | yds + mds + dds
569 |
570 |
571 | daysFromYearMonth : Int -> Int -> Int
572 | daysFromYearMonth year month =
573 | let
574 | go year month acc =
575 | if month == 0 then
576 | acc
577 | else
578 | go year (month - 1) (acc + unsafeDaysInMonth year month)
579 | in
580 | go year (month - 1) 0
581 |
582 |
583 | daysFromYear : Int -> Int
584 | daysFromYear y =
585 | if y > 0 then
586 | 366
587 | + ((y - 1) * 365)
588 | + ((y - 1) // 4)
589 | - ((y - 1) // 100)
590 | + ((y - 1) // 400)
591 | else if y < 0 then
592 | (y * 365)
593 | + (y // 4)
594 | - (y // 100)
595 | + (y // 400)
596 | else
597 | 0
598 |
599 |
600 | yearFromDays : Int -> Int
601 | yearFromDays ds =
602 | let
603 | y =
604 | ds // 365
605 |
606 | d =
607 | daysFromYear y
608 | in
609 | if ds <= d then
610 | y - 1
611 | else
612 | y
613 |
614 |
615 | dateFromDays : Int -> Date
616 | dateFromDays ds =
617 | let
618 | d400 =
619 | daysFromYear 400
620 |
621 | y400 =
622 | ds // d400
623 |
624 | d =
625 | rem ds d400
626 |
627 | year =
628 | yearFromDays (d + 1)
629 |
630 | leap =
631 | if isLeapYear year then
632 | (+) 1
633 | else
634 | identity
635 |
636 | doy =
637 | d - daysFromYear year
638 |
639 | ( month, day ) =
640 | if doy < 31 then
641 | ( 1, doy + 1 )
642 | else if doy < leap 59 then
643 | ( 2, doy - 31 + 1 )
644 | else if doy < leap 90 then
645 | ( 3, doy - leap 59 + 1 )
646 | else if doy < leap 120 then
647 | ( 4, doy - leap 90 + 1 )
648 | else if doy < leap 151 then
649 | ( 5, doy - leap 120 + 1 )
650 | else if doy < leap 181 then
651 | ( 6, doy - leap 151 + 1 )
652 | else if doy < leap 212 then
653 | ( 7, doy - leap 181 + 1 )
654 | else if doy < leap 243 then
655 | ( 8, doy - leap 212 + 1 )
656 | else if doy < leap 273 then
657 | ( 9, doy - leap 243 + 1 )
658 | else if doy < leap 304 then
659 | ( 10, doy - leap 273 + 1 )
660 | else if doy < leap 334 then
661 | ( 11, doy - leap 304 + 1 )
662 | else
663 | ( 12, doy - leap 334 + 1 )
664 | in
665 | Date
666 | { year = year + y400 * 400
667 | , month = month
668 | , day = day
669 | }
670 |
671 |
672 | clampMonth : Int -> Int
673 | clampMonth month =
674 | clamp 1 12 month
675 |
676 |
677 | clampDay : Int -> Int
678 | clampDay day =
679 | clamp 1 31 day
680 |
--------------------------------------------------------------------------------
/src/Time/DateTime.elm:
--------------------------------------------------------------------------------
1 | module Time.DateTime
2 | exposing
3 | ( DateTime
4 | , DateTimeDelta
5 | , addDays
6 | , addHours
7 | , addMilliseconds
8 | , addMinutes
9 | , addMonths
10 | , addSeconds
11 | , addYears
12 | , compare
13 | , date
14 | , dateTime
15 | , day
16 | , delta
17 | , epoch
18 | , fromTimestamp
19 | , fromTuple
20 | , hour
21 | , isValidTime
22 | , millisecond
23 | , minute
24 | , makeDateTime
25 | , month
26 | , second
27 | , setDate
28 | , setDay
29 | , setHour
30 | , setMillisecond
31 | , setMinute
32 | , setMonth
33 | , setSecond
34 | , setYear
35 | , toISO8601
36 | , toTimestamp
37 | , toTuple
38 | , weekday
39 | , year
40 | , zero
41 | )
42 |
43 | {-| This module defines a time representation based on a Date and the
44 | time of day.
45 |
46 |
47 | # DateTimes
48 |
49 | @docs DateTime
50 |
51 |
52 | # Constructing DateTimes
53 |
54 | @docs zero, epoch, dateTime, makeDateTime, fromTimestamp, toTimestamp, fromTuple, toTuple
55 |
56 |
57 | # Inspecting DateTimes
58 |
59 | @docs date, year, month, day, weekday, hour, minute, second, millisecond
60 |
61 |
62 | # Manipulating DateTimes
63 |
64 | @docs setDate, setYear, setMonth, setDay, setHour, setMinute, setSecond, setMillisecond, addYears, addMonths, addDays, addHours, addMinutes, addSeconds, addMilliseconds
65 |
66 |
67 | # Comparing DateTimes
68 |
69 | @docs compare, DateTimeDelta, delta
70 |
71 |
72 | # Helper functions
73 |
74 | @docs isValidTime
75 |
76 |
77 | # Deprecated
78 |
79 | @docs toISO8601
80 |
81 | -}
82 |
83 | import Time exposing (Time)
84 | import Time.Date exposing (Date, Weekday, isValidDate)
85 | import Time.Internal exposing (..)
86 |
87 |
88 | {-| DateTime is the opaque type for all DateTime values. Values of this
89 | type represent a valid Date and a time offset from midnight.
90 | -}
91 | type DateTime
92 | = DateTime
93 | { date : Date
94 | , offset : Int
95 | }
96 |
97 |
98 | {-| DateTimeDelta represents the difference between two
99 | DateTime values in terms of each of the different "units".
100 |
101 | See `Time.DateTime.delta` for an "aha!" example.
102 |
103 | -}
104 | type alias DateTimeDelta =
105 | { years : Int
106 | , months : Int
107 | , days : Int
108 | , hours : Int
109 | , minutes : Int
110 | , seconds : Int
111 | , milliseconds : Int
112 | }
113 |
114 |
115 | {-| zero represents the first millisecond of the first day of the
116 | current era. Use it to construct `DateTime` values:
117 |
118 | dateTime zero
119 | |> toISO8601
120 | --> "0-01-01T00:00:00.000Z"
121 |
122 | dateTime { zero | year = 2016 }
123 | |> toISO8601
124 | --> "2016-01-01T00:00:00.000Z"
125 |
126 | dateTime { zero | year = 2016, month = 5, day = 29, hour = 13 }
127 | |> toISO8601
128 | --> "2016-05-29T13:00:00.000Z"
129 |
130 | -}
131 | zero : DateTimeData
132 | zero =
133 | Time.Internal.zero
134 |
135 |
136 | {-| epoch is the instant in time that represents the first millisecond
137 | of the UNIX Epoch.
138 |
139 | epoch
140 | |> toISO8601
141 | --> "1970-01-01T00:00:00.000Z"
142 |
143 | -}
144 | epoch : DateTime
145 | epoch =
146 | dateTime { zero | year = 1970 }
147 |
148 |
149 | {-| dateTime constructs a DateTime value given a date and a time.
150 | Invalid values are clamped to the nearest valid date and time.
151 |
152 | import Time.Date
153 |
154 | dateTime { year = 2018
155 | , month = 13 -- will be clamped
156 | , day = 25
157 | , hour = 0
158 | , minute = 0
159 | , second = 0
160 | , millisecond = 47
161 | }
162 | |> date
163 | --> Time.Date.date 2018 12 25
164 |
165 | dateTime { year = 2018
166 | , month = 13
167 | , day = 25
168 | , hour = 0
169 | , minute = 0
170 | , second = 0
171 | , millisecond = 47
172 | }
173 | |> millisecond
174 | --> 47
175 |
176 | -}
177 | dateTime : DateTimeData -> DateTime
178 | dateTime ({ year, month, day } as data) =
179 | DateTime
180 | { date = Time.Date.date year month day
181 | , offset = offsetFromTimeData data
182 | }
183 |
184 |
185 | mkDateTime : Date -> TimeData d -> DateTime
186 | mkDateTime date time =
187 | DateTime { date = date, offset = offsetFromTimeData time }
188 |
189 |
190 | {-| Create a DateTime given its date and millisecond offset
191 |
192 | import Time.Date
193 |
194 | makeDateTime (Time.Date.date 2018 13 26) 1047
195 | |> date
196 | --> Time.Date.date 2018 13 26
197 |
198 | makeDateTime (Time.Date.date 2018 13 26) 1047
199 | |> millisecond
200 | --> 47
201 |
202 | makeDateTime (Time.Date.date 2018 13 26) 1047
203 | |> second
204 | --> 1
205 |
206 | -}
207 | makeDateTime : Date -> Int -> DateTime
208 | makeDateTime date offset =
209 | DateTime { date = date, offset = offset }
210 |
211 |
212 | {-| date returns a DateTime's Date.
213 |
214 | import Time.Date
215 |
216 | dateTime { year = 2018
217 | , month = 0 -- will be clamped
218 | , day = 25
219 | , hour = 0
220 | , minute = 0
221 | , second = 0
222 | , millisecond = 47
223 | }
224 | |> date
225 | --> Time.Date.date 2018 1 25
226 |
227 | -}
228 | date : DateTime -> Date
229 | date (DateTime { date }) =
230 | date
231 |
232 |
233 | {-| year returns a DateTime's year.
234 |
235 | dateTime { zero | year = 2015 }
236 | |> year
237 | --> 2015
238 |
239 | -}
240 | year : DateTime -> Int
241 | year (DateTime { date }) =
242 | Time.Date.year date
243 |
244 |
245 | {-| month returns a DateTime's month.
246 |
247 | dateTime { zero | month = 7 }
248 | |> month
249 | --> 7
250 |
251 | dateTime { zero | month = 0 } -- will be clamped
252 | |> month
253 | --> 1
254 |
255 | -}
256 | month : DateTime -> Int
257 | month (DateTime { date }) =
258 | Time.Date.month date
259 |
260 |
261 | {-| day returns a DateTime's day.
262 |
263 | dateTime { zero | day = 31 }
264 | |> day
265 | --> 31
266 |
267 | dateTime { zero | day = 32 } -- will be clamped
268 | |> day
269 | --> 31
270 |
271 | -}
272 | day : DateTime -> Int
273 | day (DateTime { date }) =
274 | Time.Date.day date
275 |
276 |
277 | {-| weekday returns a DateTime's day of the week.
278 |
279 | import Time.Date
280 |
281 | dateTime { zero | year = 2018, month = 5, day = 27 }
282 | |> weekday
283 | --> Time.Date.Sun
284 |
285 | -}
286 | weekday : DateTime -> Weekday
287 | weekday (DateTime { date }) =
288 | Time.Date.weekday date
289 |
290 |
291 | {-| hour returns a DateTime's hour.
292 |
293 | dateTime { zero | hour = 23 }
294 | |> hour
295 | --> 23
296 |
297 | dateTime { zero | hour = 24 } -- will be clamped
298 | |> hour
299 | --> 23
300 |
301 | -}
302 | hour : DateTime -> Int
303 | hour (DateTime { offset }) =
304 | offset // hourMs
305 |
306 |
307 | {-| minute returns a DateTime's minute.
308 |
309 | dateTime { zero | minute = 59 }
310 | |> minute
311 | --> 59
312 |
313 | dateTime { zero | minute = 60 } -- will be clamped
314 | |> minute
315 | --> 59
316 |
317 | -}
318 | minute : DateTime -> Int
319 | minute (DateTime { offset }) =
320 | (offset % hourMs) // minuteMs
321 |
322 |
323 | {-| second returns a DateTime's second.
324 |
325 | dateTime { zero | second = 59 }
326 | |> second
327 | --> 59
328 |
329 | dateTime { zero | second = 60 } -- will be clamped
330 | |> second
331 | --> 59
332 |
333 | -}
334 | second : DateTime -> Int
335 | second (DateTime { offset }) =
336 | (offset % hourMs % minuteMs) // secondMs
337 |
338 |
339 | {-| millisecond returns a DateTime's millisecond.
340 |
341 | dateTime { zero | millisecond = 999 }
342 | |> millisecond
343 | --> 999
344 |
345 | dateTime { zero | millisecond = 1000 } -- will be clamped
346 | |> millisecond
347 | --> 999
348 |
349 | -}
350 | millisecond : DateTime -> Int
351 | millisecond (DateTime { offset }) =
352 | offset % hourMs % minuteMs % secondMs
353 |
354 |
355 | {-| setDate sets a DateTime's Date.
356 |
357 | import Time.Date as TD
358 |
359 | dateTime zero
360 | |> setDate (TD.date 2018 5 27)
361 | |> date
362 | --> TD.date 2018 5 27
363 |
364 | -}
365 | setDate : Date -> DateTime -> DateTime
366 | setDate date (DateTime { offset }) =
367 | DateTime
368 | { date = date
369 | , offset = offset
370 | }
371 |
372 |
373 | {-| setYear sets a DateTime's year.
374 |
375 | See also `Time.Date.setYear`.
376 |
377 | dateTime zero
378 | |> setYear 2018
379 | |> year
380 | --> 2018
381 |
382 | -}
383 | setYear : Int -> DateTime -> DateTime
384 | setYear year (DateTime { date, offset }) =
385 | DateTime
386 | { date = Time.Date.setYear year date
387 | , offset = offset
388 | }
389 |
390 |
391 | {-| setMonth sets a DateTime's month.
392 |
393 | See also `Time.Date.setMonth`.
394 |
395 | dateTime zero
396 | |> setMonth 12
397 | |> month
398 | --> 12
399 |
400 | -}
401 | setMonth : Int -> DateTime -> DateTime
402 | setMonth month (DateTime { date, offset }) =
403 | DateTime
404 | { date = Time.Date.setMonth month date
405 | , offset = offset
406 | }
407 |
408 |
409 | {-| setDay sets a DateTime's day.
410 |
411 | See also `Time.Date.setDay`.
412 |
413 | dateTime zero
414 | |> setDay 31
415 | |> day
416 | --> 31
417 |
418 | -}
419 | setDay : Int -> DateTime -> DateTime
420 | setDay day (DateTime { date, offset }) =
421 | DateTime
422 | { date = Time.Date.setDay day date
423 | , offset = offset
424 | }
425 |
426 |
427 | {-| setHour sets a DateTime's hour.
428 |
429 | dateTime zero
430 | |> setHour 23
431 | |> hour
432 | --> 23
433 |
434 | -}
435 | setHour : Int -> DateTime -> DateTime
436 | setHour hour ((DateTime { date }) as t) =
437 | mkDateTime date
438 | { hour = hour
439 | , minute = minute t
440 | , second = second t
441 | , millisecond = millisecond t
442 | }
443 |
444 |
445 | {-| setMinute sets a DateTime's minute.
446 |
447 | dateTime zero
448 | |> setMinute 59
449 | |> minute
450 | --> 59
451 |
452 | -}
453 | setMinute : Int -> DateTime -> DateTime
454 | setMinute minute ((DateTime { date }) as t) =
455 | mkDateTime date
456 | { hour = hour t
457 | , minute = minute
458 | , second = second t
459 | , millisecond = millisecond t
460 | }
461 |
462 |
463 | {-| setSecond sets a DateTime's second.
464 | dateTime zero
465 | |> setSecond 59
466 | |> second
467 | --> 59
468 | -}
469 | setSecond : Int -> DateTime -> DateTime
470 | setSecond second ((DateTime { date }) as t) =
471 | mkDateTime date
472 | { hour = hour t
473 | , minute = minute t
474 | , second = second
475 | , millisecond = millisecond t
476 | }
477 |
478 |
479 | {-| setMillisecond sets a DateTime's millisecond.
480 |
481 | dateTime zero
482 | |> setMillisecond 999
483 | |> millisecond
484 | --> 999
485 |
486 | -}
487 | setMillisecond : Int -> DateTime -> DateTime
488 | setMillisecond millisecond ((DateTime { date }) as t) =
489 | mkDateTime date
490 | { hour = hour t
491 | , minute = minute t
492 | , second = second t
493 | , millisecond = millisecond
494 | }
495 |
496 |
497 | {-| addYears adds a relative number of years to a DateTime value.
498 |
499 | See also `Time.Date.addYears`.
500 |
501 | dateTime { zero | year = 2016 }
502 | |> addYears 2
503 | |> year
504 | --> 2018
505 |
506 | -}
507 | addYears : Int -> DateTime -> DateTime
508 | addYears years (DateTime { date, offset }) =
509 | DateTime
510 | { date = Time.Date.addYears years date
511 | , offset = offset
512 | }
513 |
514 |
515 | {-| addMonths adds a relative number of months to a DateTime value.
516 |
517 | See also `Time.Date.addMonths`.
518 |
519 | dateTime { zero | month = 1 }
520 | |> addMonths 1
521 | |> month
522 | --> 2
523 |
524 | -}
525 | addMonths : Int -> DateTime -> DateTime
526 | addMonths months (DateTime { date, offset }) =
527 | DateTime
528 | { date = Time.Date.addMonths months date
529 | , offset = offset
530 | }
531 |
532 |
533 | {-| addDays adds a relative number of days to a DateTime value.
534 |
535 | See also `Time.Date.addDays`.
536 |
537 | dateTime { zero | day = 20 }
538 | |> addDays -11
539 | |> day
540 | --> 9
541 |
542 | -}
543 | addDays : Int -> DateTime -> DateTime
544 | addDays days (DateTime { date, offset }) =
545 | DateTime
546 | { date = Time.Date.addDays days date
547 | , offset = offset
548 | }
549 |
550 |
551 | {-| addHours adds a relative number of hours to a DateTime value.
552 |
553 | dateTime { zero | hour = 23 }
554 | |> addHours 1
555 | |> hour
556 | --> 0
557 |
558 | -}
559 | addHours : Int -> DateTime -> DateTime
560 | addHours hours time =
561 | addMilliseconds (hours * hourMs) time
562 |
563 |
564 | {-| addMinutes adds a relative number of minutes to a DateTime value.
565 |
566 | dateTime { zero | minute = 30 }
567 | |> addMinutes 30
568 | |> minute
569 | --> 0
570 |
571 | -}
572 | addMinutes : Int -> DateTime -> DateTime
573 | addMinutes minutes time =
574 | addMilliseconds (minutes * minuteMs) time
575 |
576 |
577 | {-| addSeconds adds a relative number of seconds to a DateTime value.
578 |
579 | dateTime { zero | second = 59 }
580 | |> addSeconds 1
581 | |> second
582 | --> 0
583 |
584 | -}
585 | addSeconds : Int -> DateTime -> DateTime
586 | addSeconds seconds time =
587 | addMilliseconds (seconds * secondMs) time
588 |
589 |
590 | {-| addMilliseconds adds an absolute number of milliseconds to a
591 | DateTime value.
592 |
593 | dateTime { zero | second = 10, millisecond = 1 }
594 | |> addMilliseconds 999
595 | |> second
596 | --> 11
597 |
598 | -}
599 | addMilliseconds : Int -> DateTime -> DateTime
600 | addMilliseconds ms (DateTime { date, offset }) =
601 | let
602 | total =
603 | ms + offset
604 |
605 | ( days, newOffset ) =
606 | if total < 0 then
607 | let
608 | days =
609 | -(abs total // dayMs + 1)
610 |
611 | offset =
612 | rem total dayMs
613 | in
614 | if offset == 0 then
615 | ( days + 1, 0 )
616 | else
617 | ( days, dayMs + rem offset dayMs )
618 | else
619 | ( total // dayMs, rem total dayMs )
620 | in
621 | DateTime
622 | { date = Time.Date.addDays days date
623 | , offset = newOffset
624 | }
625 |
626 |
627 | {-| compare two DateTimes.
628 |
629 | import Basics exposing (Order(..))
630 |
631 | epoch
632 | |> addMilliseconds -1
633 | |> Time.DateTime.compare (dateTime
634 | { year = 1969
635 | , month = 12
636 | , day = 31
637 | , hour = 23
638 | , minute = 59
639 | , second = 59
640 | , millisecond = 999
641 | }
642 | )
643 | --> EQ
644 |
645 | -}
646 | compare : DateTime -> DateTime -> Order
647 | compare dt1 dt2 =
648 | -- comparison of 7-tuples is not supported so we use toISO8601 instead
649 | Basics.compare (toISO8601 dt1) (toISO8601 dt2)
650 |
651 |
652 | {-| delta computes the relative difference between two DateTime values.
653 | See also `Time.Date.delta`.
654 |
655 | upper : DateTime
656 | upper = dateTime
657 | { year = 1970
658 | , month = 1
659 | , day = 1
660 | , hour = 0
661 | , minute = 0
662 | , second = 0
663 | , millisecond = 0
664 | }
665 |
666 | upper
667 | |> addYears -1
668 | |> delta upper
669 | --> { years = 1
670 | --> , months = 12
671 | --> , days = 365
672 | --> , hours = 8760
673 | --> , minutes = 525600
674 | --> , seconds = 31536000
675 | --> , milliseconds = 31536000000
676 | --> }
677 |
678 | -- Note what is counted is the number of transitions
679 | -- to get from one unit to another. Hence
680 | -- the following shows that the difference of
681 | -- 1 day makes a big difference when it separates
682 | -- Dec 31 and Jan 1 as shown here. Observe
683 | -- that years, months, and days are the same because
684 | -- each only requires one transition to get from
685 | -- one date to the other.
686 |
687 | upper
688 | |> addDays -1
689 | |> delta upper
690 | --> { years = 1
691 | --> , months = 1
692 | --> , days = 1
693 | --> , hours = 24
694 | --> , minutes = 1440
695 | --> , seconds = 86400
696 | --> , milliseconds = 86400000
697 | --> }
698 |
699 | -}
700 | delta : DateTime -> DateTime -> DateTimeDelta
701 | delta (DateTime t1) (DateTime t2) =
702 | let
703 | { years, months, days } =
704 | Time.Date.delta t1.date t2.date
705 |
706 | milliseconds =
707 | days * dayMs + (t1.offset - t2.offset)
708 |
709 | hours =
710 | milliseconds // hourMs
711 |
712 | minutes =
713 | milliseconds // minuteMs
714 |
715 | seconds =
716 | milliseconds // secondMs
717 | in
718 | { years = years
719 | , months = months
720 | , days = days
721 | , hours = hours
722 | , minutes = minutes
723 | , seconds = seconds
724 | , milliseconds = milliseconds
725 | }
726 |
727 |
728 | {-| isValidTime returns True if the given hour, minute, second and
729 | millisecond represent a valid time of day.
730 |
731 | isValidTime 24 0 0 0
732 | --> False
733 |
734 | isValidTime 23 59 59 999
735 | --> True
736 |
737 | -}
738 | isValidTime : Int -> Int -> Int -> Int -> Bool
739 | isValidTime hour minute second millisecond =
740 | hour >= 0 && hour < 24 && minute >= 0 && minute < 60 && second >= 0 && second < 60 && millisecond >= 0 && millisecond < 1000
741 |
742 |
743 | {-| toTimestamp converts a DateTime value to its UNIX timestamp
744 | representation as milliseconds.
745 |
746 | epoch
747 | |> toTimestamp
748 | --> 0.0
749 |
750 | -}
751 | toTimestamp : DateTime -> Time
752 | toTimestamp time =
753 | delta time epoch
754 | |> .milliseconds
755 | |> toFloat
756 |
757 |
758 | {-| fromTimestamp converts the millisecond representation of a
759 | UNIX timestamp into a DateTime value.
760 |
761 | fromTimestamp 0.0
762 | --> epoch
763 |
764 | -}
765 | fromTimestamp : Time -> DateTime
766 | fromTimestamp timestamp =
767 | addMilliseconds (round timestamp) epoch
768 |
769 |
770 | {-| toTuple converts a DateTime into a (year, month, day, hour, miunte,
771 | second, millisecond) tuple.
772 |
773 | DEPRECATED - unavailable in Elm 0.19
774 |
775 | toTuple epoch
776 | --> (1970, 1, 1, 0, 0, 0, 0)
777 |
778 | -}
779 | toTuple : DateTime -> ( Int, Int, Int, Int, Int, Int, Int )
780 | toTuple ((DateTime { date }) as t) =
781 | let
782 | ( year, month, day ) =
783 | Time.Date.toTuple date
784 | in
785 | ( year, month, day, hour t, minute t, second t, millisecond t )
786 |
787 |
788 | {-| fromTuple converts a (year, month, day, hour, minute, second,
789 | millisecond) tuple into a DateTime.
790 |
791 | DEPRECATED - unavailable in Elm 0.19
792 |
793 | fromTuple (1970, 1, 1, 0, 0, 0, 0)
794 | --> epoch
795 |
796 | -}
797 | fromTuple : ( Int, Int, Int, Int, Int, Int, Int ) -> DateTime
798 | fromTuple ( year, month, day, hour, minute, second, millisecond ) =
799 | dateTime
800 | { year = year
801 | , month = month
802 | , day = day
803 | , hour = hour
804 | , minute = minute
805 | , second = second
806 | , millisecond = millisecond
807 | }
808 |
809 |
810 | {-| toISO8601 renders a DateTime in ISO8601 format.
811 |
812 | DEPRECATED: this is used as a hack for the compare function above; it is not exposed --
813 | use the functionality in Iso8601 instead.
814 |
815 | -}
816 | toISO8601 : DateTime -> String
817 | toISO8601 time =
818 | toString (year time)
819 | ++ "-"
820 | ++ padded (month time)
821 | ++ "-"
822 | ++ padded (day time)
823 | ++ "T"
824 | ++ padded (hour time)
825 | ++ ":"
826 | ++ padded (minute time)
827 | ++ ":"
828 | ++ padded (second time)
829 | ++ "."
830 | ++ padded3 (millisecond time)
831 | ++ "Z"
832 |
--------------------------------------------------------------------------------