├── .gitignore ├── LICENSE ├── README.md ├── elm.json └── src ├── Elm └── Kernel │ ├── Time.js │ └── Time.server.js └── Time.elm /.gitignore: -------------------------------------------------------------------------------- 1 | elm-stuff -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018-present, Evan Czaplicki 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of Evan Czaplicki nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Time 2 | 3 | To work with time successfully in programming, we need three different concepts: 4 | 5 | - **Human Time** — This is what you see on clocks (8am) or on calendars (May 3rd). Great! But if my phone call is at 8am in Boston, what time is it for my friend in Vancouver? If it is at 8am in Tokyo, is that even the same day in New York? (No!) So between [time zones][tz] based on ever-changing political boundaries and inconsistent use of [daylight saving time][dst], human time should basically never be stored in your `Model` or database! It is only for display! 6 | 7 | - **POSIX Time** — With POSIX time, it does not matter where you live or what time of year it is. It is just the number of seconds elapsed since some arbitrary moment (in 1970). Everywhere you go on Earth, POSIX time is the same. 8 | 9 | - **Time Zones** — A “time zone” is a bunch of data that allows you to turn POSIX time into human time. This is _not_ just `UTC-7` or `UTC+3` though! Time zones are way more complicated than a simple offset! Every time [Florida switches to DST forever][florida] or [Samoa switches from UTC-11 to UTC+13][samoa], some poor soul adds a note to the [IANA time zone database][iana]. That database is loaded onto every computer, and between POSIX time and all the corner cases in the database, we can figure out human times! 10 | 11 | [tz]: https://en.wikipedia.org/wiki/Time_zone 12 | [dst]: https://en.wikipedia.org/wiki/Daylight_saving_time 13 | [iana]: https://en.wikipedia.org/wiki/IANA_time_zone_database 14 | [samoa]: https://en.wikipedia.org/wiki/Time_in_Samoa 15 | [florida]: https://www.npr.org/sections/thetwo-way/2018/03/08/591925587/ 16 | 17 | So to show a human being a time, you must always know **the POSIX time** and **their time zone**. That is it. So all that “human time” stuff is for your `view` function, not your `Model`. 18 | 19 | 20 | ## Example 21 | 22 | To figure out a human time, you need to ask two questions: (1) what POSIX time is it? and (2) what time zone am I in? Once you have that, you can decide how to show it on screen: 23 | 24 | ```elm 25 | import Time exposing (utc, toHour, toMinute, toSecond) 26 | 27 | toUtcString : Time.Posix -> String 28 | toUtcString time = 29 | String.fromInt (toHour utc time) 30 | ++ ":" ++ 31 | String.fromInt (toMinute utc time) 32 | ++ ":" ++ 33 | String.fromInt (toSecond utc time) 34 | ++ " (UTC)" 35 | ``` 36 | 37 | Notice that we provide the `utc` time zone to `toHour` and `toMinute`! 38 | 39 | Go [here](https://elm-lang.org/examples/time) for a little example application that uses time. It can help you get everything hooked up in practice! 40 | 41 | 42 | ## Recurring Events 43 | 44 | A lot of programmers need to handle recurring events. This meeting repeats every Monday. This event is the first Wednesday of each month. And there are always exceptions where a recurring event gets moved! **Using human time does not solve this!** 45 | 46 | To properly handle recurring events, you need to create a custom `type` for your particular problem. Say you want to model a weekly event: 47 | 48 | ```elm 49 | import Time 50 | 51 | type alias WeeklyEvent = 52 | { weekday : Time.Weekday -- which day is it on 53 | , hour : Int -- at what hour? 54 | , zone : Time.Zone -- what time zone is that hour in? 55 | , start : Time.Posix -- when was the first event? 56 | , exceptions : List (Int, Maybe Event) -- are there any skips? 57 | } 58 | ``` 59 | 60 | The first two fields (`weekday` and `hour`) are the most straight forward. You gotta know what day and what time! But that is not enough information for people in different time zones. Tom created the recurring event for hour 16, but how do I show that in Tokyo or New York? Or even in Tom’s location?! The `zone` lets us pin `weekday` and `hour` to a specific POSIX time, so we can show it elsewhere. 61 | 62 | Great! But what about shifting the meeting by one day for a holiday? Well, if you define a `start` time, you can store `exceptions` as offsets from the first ever event. So if _only_ the third event was cancelled, you could store `[ (3, Nothing) ]` which would say “ignore the third event, and do not replace it with some other specific event.” 63 | 64 | ### Implications 65 | 66 | Now the particular kinds of recurring events _you_ need are specific to _your_ application. Weekly? Monthly? Always has start and end of range? Allows exceptions? I am not convinced that a generic design is possible for all scenarios, but maybe with further exploration, we will find that it is. 67 | 68 | **So if you need recurring events, you have to model them yourself.** There is no shortcut. Putting May 3rd in your `Model` is not gonna do it. It is a trap. Thinking in human time is always a trap! 69 | 70 | 71 | ## ISO 8601 72 | 73 | [ISO 8601][8601] is not supported by this package because: 74 | 75 | > The ISO 8601 format has lead to a great deal of confusion. Specifically because it gives the _illusion_ that it can handle time zones. It cannot! It allows you to specify an offset from UTC like `-05:00`, but is that a time in Quebec, Miami, Cuba, or Equador? Are they doing daylight saving time right now? 76 | > 77 | > Point is, **the only thing ISO 8601 is good for is representing a `Time.Posix`, but less memory efficient and more confusing.** So I recommend using `Time.posixToMillis` and `Time.millisToPosix` for any client/server communication you control. 78 | 79 | That said, many endpoints use ISO 8601 for some reason, and it can therefore be quite useful in practice. I think the community should make some packages that define `fromIso8601 : String -> Maybe Time.Posix` in Elm. People can use `elm/parser` to make a fancy implementation, but maybe there is some faster or smaller implementation possible with `String.split` and such. Folks should experiment, and later on, we can revisit if any of them belong in this library. 80 | 81 | [8601]: https://en.wikipedia.org/wiki/ISO_8601 82 | 83 | 84 | ## Future Plans 85 | 86 | Right now this library gives basic `Posix` and `Zone` functions, but there are a couple important things it does not cover right now: 87 | 88 | 1. How do I get _my_ time zone? 89 | 2. How do I get another time zone by name? 90 | 3. How do I display a time for a specific region? (e.g. `DD/MM/YYYY` vs `MM/DD/YYYY`) 91 | 92 | I think points (2) and (3) should be explored by the community before we add anything here. Maybe we can have a package that hard codes the IANA time zone database? Maybe we can have a package that provides HTTP requests to ask for specific time zone data? Etc. 93 | 94 | **Note:** If you make progress that potentially needs coordination with other developers, **talk to people**. Present your work on [discourse](https://discourse.elm-lang.org/) to learn what the next steps might be. Is the idea good? Does it need more work? Are there other things to consider? Etc. Just opening an issue like “I totally redid the API” is not how we run things here, so focus on making a strong case through normal packages and friendly communication! And on timelines, we try to make one _great_ choice ever (not a _different_ choice every month) so things will take longer than in the JS world. 95 | -------------------------------------------------------------------------------- /elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "package", 3 | "name": "elm/time", 4 | "summary": "Work with POSIX times, time zones, years, months, days, hours, seconds, etc.", 5 | "license": "BSD-3-Clause", 6 | "version": "1.0.0", 7 | "exposed-modules": [ 8 | "Time" 9 | ], 10 | "elm-version": "0.19.0 <= v < 0.20.0", 11 | "dependencies": { 12 | "elm/core": "1.0.0 <= v < 2.0.0" 13 | }, 14 | "test-dependencies": {} 15 | } -------------------------------------------------------------------------------- /src/Elm/Kernel/Time.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | import Time exposing (customZone, Name, Offset) 4 | import Elm.Kernel.List exposing (Nil) 5 | import Elm.Kernel.Scheduler exposing (binding, succeed) 6 | 7 | */ 8 | 9 | 10 | function _Time_now(millisToPosix) 11 | { 12 | return __Scheduler_binding(function(callback) 13 | { 14 | callback(__Scheduler_succeed(millisToPosix(Date.now()))); 15 | }); 16 | } 17 | 18 | var _Time_setInterval = F2(function(interval, task) 19 | { 20 | return __Scheduler_binding(function(callback) 21 | { 22 | var id = setInterval(function() { _Scheduler_rawSpawn(task); }, interval); 23 | return function() { clearInterval(id); }; 24 | }); 25 | }); 26 | 27 | function _Time_here() 28 | { 29 | return __Scheduler_binding(function(callback) 30 | { 31 | callback(__Scheduler_succeed( 32 | A2(__Time_customZone, -(new Date().getTimezoneOffset()), __List_Nil) 33 | )); 34 | }); 35 | } 36 | 37 | 38 | function _Time_getZoneName() 39 | { 40 | return __Scheduler_binding(function(callback) 41 | { 42 | try 43 | { 44 | var name = __Time_Name(Intl.DateTimeFormat().resolvedOptions().timeZone); 45 | } 46 | catch (e) 47 | { 48 | var name = __Time_Offset(new Date().getTimezoneOffset()); 49 | } 50 | callback(__Scheduler_succeed(name)); 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /src/Elm/Kernel/Time.server.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | import Elm.Kernel.Scheduler exposing (binding) 4 | 5 | */ 6 | 7 | 8 | function _Time_neverResolve() 9 | { 10 | return __Scheduler_binding(function() {}); 11 | } 12 | 13 | var _Time_now = _Time_neverResolve; 14 | var _Time_here = _Time_neverResolve; 15 | var _Time_getZoneName = _Time_neverResolve; 16 | var _Time_setInterval = F2(_Time_neverResolve); 17 | -------------------------------------------------------------------------------- /src/Time.elm: -------------------------------------------------------------------------------- 1 | effect module Time where { subscription = MySub } exposing 2 | ( Posix 3 | , now 4 | , every 5 | , posixToMillis 6 | , millisToPosix 7 | , Zone 8 | , utc 9 | , here 10 | , toYear 11 | , toMonth 12 | , toDay 13 | , toWeekday 14 | , toHour 15 | , toMinute 16 | , toSecond 17 | , toMillis 18 | , Month(..) 19 | , Weekday(..) 20 | , customZone 21 | , getZoneName 22 | , ZoneName(..) 23 | ) 24 | 25 | 26 | {-| Library for working with time and time zones. 27 | 28 | # Time 29 | @docs Posix, now, every, posixToMillis, millisToPosix 30 | 31 | # Time Zones 32 | @docs Zone, utc, here 33 | 34 | # Human Times 35 | @docs toYear, toMonth, toDay, toWeekday, toHour, toMinute, toSecond, toMillis 36 | 37 | # Weeks and Months 38 | @docs Weekday, Month 39 | 40 | # For Package Authors 41 | @docs customZone, getZoneName, ZoneName 42 | 43 | -} 44 | 45 | 46 | import Basics exposing (..) 47 | import Dict 48 | import Elm.Kernel.Time 49 | import List exposing ((::)) 50 | import Maybe exposing (Maybe(..)) 51 | import Platform 52 | import Platform.Sub exposing (Sub) 53 | import Process 54 | import String exposing (String) 55 | import Task exposing (Task) 56 | 57 | 58 | 59 | -- POSIX 60 | 61 | 62 | {-| A computer representation of time. It is the same all over Earth, so if we 63 | have a phone call or meeting at a certain POSIX time, there is no ambiguity. 64 | 65 | It is very hard for humans to _read_ a POSIX time though, so we use functions 66 | like [`toHour`](#toHour) and [`toMinute`](#toMinute) to `view` them. 67 | -} 68 | type Posix = Posix Int 69 | 70 | 71 | {-| Get the POSIX time at the moment when this task is run. 72 | -} 73 | now : Task x Posix 74 | now = 75 | Elm.Kernel.Time.now millisToPosix 76 | 77 | 78 | {-| Turn a `Posix` time into the number of milliseconds since 1970 January 1 79 | at 00:00:00 UTC. It was a Thursday. 80 | -} 81 | posixToMillis : Posix -> Int 82 | posixToMillis (Posix millis) = 83 | millis 84 | 85 | 86 | {-| Turn milliseconds into a `Posix` time. 87 | -} 88 | millisToPosix : Int -> Posix 89 | millisToPosix = 90 | Posix 91 | 92 | 93 | 94 | -- TIME ZONES 95 | 96 | 97 | {-| Information about a particular time zone. 98 | 99 | The [IANA Time Zone Database][iana] tracks things like UTC offsets and 100 | daylight-saving rules so that you can turn a `Posix` time into local times 101 | within a time zone. 102 | 103 | See [`utc`](#utc) and [`here`](#here) to learn how to obtain `Zone` values. 104 | 105 | [iana]: https://www.iana.org/time-zones 106 | -} 107 | type Zone = 108 | Zone Int (List Era) 109 | 110 | 111 | -- TODO: add this note back to `Zone` docs when it is true 112 | -- 113 | -- Did you know that in California the times change from 3pm PST to 3pm PDT to 114 | -- capture whether it is daylight-saving time? The database tracks those 115 | -- abbreviation changes too. (Tons of time zones do that actually.) 116 | -- 117 | 118 | 119 | {-| Currently the public API only needs: 120 | 121 | - `start` is the beginning of this `Era` in "minutes since the Unix Epoch" 122 | - `offset` is the UTC offset of this `Era` in minutes 123 | 124 | But eventually, it will make sense to have `abbr : String` for `PST` vs `PDT` 125 | -} 126 | type alias Era = 127 | { start : Int 128 | , offset : Int 129 | } 130 | 131 | 132 | {-| The time zone for Coordinated Universal Time ([UTC][]) 133 | 134 | The `utc` zone has no time adjustments. It never observes daylight-saving 135 | time and it never shifts around based on political restructuring. 136 | 137 | [UTC]: https://en.wikipedia.org/wiki/Coordinated_Universal_Time 138 | -} 139 | utc : Zone 140 | utc = 141 | Zone 0 [] 142 | 143 | 144 | {-| Produce a `Zone` based on the current UTC offset. You can use this to figure 145 | out what day it is where you are: 146 | 147 | import Task exposing (Task) 148 | import Time 149 | 150 | whatDayIsIt : Task x Int 151 | whatDayIsIt = 152 | Task.map2 Time.toDay Time.here Time.now 153 | 154 | **Accuracy Note:** This function can only give time zones like `Etc/GMT+9` or 155 | `Etc/GMT-6`. It cannot give you `Europe/Stockholm`, `Asia/Tokyo`, or any other 156 | normal time zone from the [full list][tz] due to limitations in JavaScript. 157 | For example, if you run `here` in New York City, the resulting `Zone` will 158 | never be `America/New_York`. Instead you get `Etc/GMT-5` or `Etc/GMT-4` 159 | depending on Daylight Saving Time. So even though browsers must have internal 160 | access to `America/New_York` to figure out that offset, there is no public API 161 | to get the full information. This means the `Zone` you get from this function 162 | will act weird if (1) an application stays open across a Daylight Saving Time 163 | boundary or (2) you try to use it on historical data. 164 | 165 | **Future Note:** We can improve `here` when there is good browser support for 166 | JavaScript functions that (1) expose the IANA time zone database and (2) let 167 | you ask the time zone of the computer. The committee that reviews additions to 168 | JavaScript is called TC39, and I encourage you to push for these capabilities! I 169 | cannot do it myself unfortunately. 170 | 171 | **Alternatives:** See the `customZone` docs to learn how to implement stopgaps. 172 | 173 | [tz]: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones 174 | -} 175 | here : Task x Zone 176 | here = 177 | Elm.Kernel.Time.here () 178 | 179 | 180 | 181 | -- DATES 182 | 183 | 184 | {-| What year is it?! 185 | 186 | import Time exposing (toYear, utc, millisToPosix) 187 | 188 | toYear utc (millisToPosix 0) == 1970 189 | toYear nyc (millisToPosix 0) == 1969 190 | 191 | -- pretend `nyc` is the `Zone` for America/New_York. 192 | -} 193 | toYear : Zone -> Posix -> Int 194 | toYear zone time = 195 | (toCivil (toAdjustedMinutes zone time)).year 196 | 197 | 198 | {-| What month is it?! 199 | 200 | import Time exposing (toMonth, utc, millisToPosix) 201 | 202 | toMonth utc (millisToPosix 0) == Jan 203 | toMonth nyc (millisToPosix 0) == Dec 204 | 205 | -- pretend `nyc` is the `Zone` for America/New_York. 206 | -} 207 | toMonth : Zone -> Posix -> Month 208 | toMonth zone time = 209 | case (toCivil (toAdjustedMinutes zone time)).month of 210 | 1 -> Jan 211 | 2 -> Feb 212 | 3 -> Mar 213 | 4 -> Apr 214 | 5 -> May 215 | 6 -> Jun 216 | 7 -> Jul 217 | 8 -> Aug 218 | 9 -> Sep 219 | 10 -> Oct 220 | 11 -> Nov 221 | _ -> Dec 222 | 223 | 224 | {-| What day is it?! (Days go from 1 to 31) 225 | 226 | import Time exposing (toDay, utc, millisToPosix) 227 | 228 | toDay utc (millisToPosix 0) == 1 229 | toDay nyc (millisToPosix 0) == 31 230 | 231 | -- pretend `nyc` is the `Zone` for America/New_York. 232 | 233 | -} 234 | toDay : Zone -> Posix -> Int 235 | toDay zone time = 236 | (toCivil (toAdjustedMinutes zone time)).day 237 | 238 | 239 | {-| What day of the week is it? 240 | 241 | import Time exposing (toWeekday, utc, millisToPosix) 242 | 243 | toWeekday utc (millisToPosix 0) == Thu 244 | toWeekday nyc (millisToPosix 0) == Wed 245 | 246 | -- pretend `nyc` is the `Zone` for America/New_York. 247 | -} 248 | toWeekday : Zone -> Posix -> Weekday 249 | toWeekday zone time = 250 | case modBy 7 (flooredDiv (toAdjustedMinutes zone time) (60 * 24)) of 251 | 0 -> Thu 252 | 1 -> Fri 253 | 2 -> Sat 254 | 3 -> Sun 255 | 4 -> Mon 256 | 5 -> Tue 257 | _ -> Wed 258 | 259 | 260 | {-| What hour is it? (From 0 to 23) 261 | 262 | import Time exposing (toHour, utc, millisToPosix) 263 | 264 | toHour utc (millisToPosix 0) == 0 -- 12am 265 | toHour nyc (millisToPosix 0) == 19 -- 7pm 266 | 267 | -- pretend `nyc` is the `Zone` for America/New_York. 268 | -} 269 | toHour : Zone -> Posix -> Int 270 | toHour zone time = 271 | modBy 24 (flooredDiv (toAdjustedMinutes zone time) 60) 272 | 273 | 274 | {-| What minute is it? (From 0 to 59) 275 | 276 | import Time exposing (toMinute, utc, millisToPosix) 277 | 278 | toMinute utc (millisToPosix 0) == 0 279 | 280 | This can be different in different time zones. Some time zones are offset 281 | by 30 or 45 minutes! 282 | -} 283 | toMinute : Zone -> Posix -> Int 284 | toMinute zone time = 285 | modBy 60 (toAdjustedMinutes zone time) 286 | 287 | 288 | {-| What second is it? 289 | 290 | import Time exposing (toSecond, utc, millisToPosix) 291 | 292 | toSecond utc (millisToPosix 0) == 0 293 | toSecond utc (millisToPosix 1234) == 1 294 | toSecond utc (millisToPosix 5678) == 5 295 | -} 296 | toSecond : Zone -> Posix -> Int 297 | toSecond _ time = 298 | modBy 60 (flooredDiv (posixToMillis time) 1000) 299 | 300 | 301 | {-| 302 | import Time exposing (toMillis, utc, millisToPosix) 303 | 304 | toMillis utc (millisToPosix 0) == 0 305 | toMillis utc (millisToPosix 1234) == 234 306 | toMillis utc (millisToPosix 5678) == 678 307 | -} 308 | toMillis : Zone -> Posix -> Int 309 | toMillis _ time = 310 | modBy 1000 (posixToMillis time) 311 | 312 | 313 | 314 | -- DATE HELPERS 315 | 316 | 317 | toAdjustedMinutes : Zone -> Posix -> Int 318 | toAdjustedMinutes (Zone defaultOffset eras) time = 319 | toAdjustedMinutesHelp defaultOffset (flooredDiv (posixToMillis time) 60000) eras 320 | 321 | 322 | toAdjustedMinutesHelp : Int -> Int -> List Era -> Int 323 | toAdjustedMinutesHelp defaultOffset posixMinutes eras = 324 | case eras of 325 | [] -> 326 | posixMinutes + defaultOffset 327 | 328 | era :: olderEras -> 329 | if era.start < posixMinutes then 330 | posixMinutes + era.offset 331 | else 332 | toAdjustedMinutesHelp defaultOffset posixMinutes olderEras 333 | 334 | 335 | toCivil : Int -> { year : Int, month : Int, day : Int } 336 | toCivil minutes = 337 | let 338 | rawDay = flooredDiv minutes (60 * 24) + 719468 339 | era = (if rawDay >= 0 then rawDay else rawDay - 146096) // 146097 340 | dayOfEra = rawDay - era * 146097 -- [0, 146096] 341 | yearOfEra = (dayOfEra - dayOfEra // 1460 + dayOfEra // 36524 - dayOfEra // 146096) // 365 -- [0, 399] 342 | year = yearOfEra + era * 400 343 | dayOfYear = dayOfEra - (365 * yearOfEra + yearOfEra // 4 - yearOfEra // 100) -- [0, 365] 344 | mp = (5 * dayOfYear + 2) // 153 -- [0, 11] 345 | month = mp + (if mp < 10 then 3 else -9) -- [1, 12] 346 | in 347 | { year = year + (if month <= 2 then 1 else 0) 348 | , month = month 349 | , day = dayOfYear - (153 * mp + 2) // 5 + 1 -- [1, 31] 350 | } 351 | 352 | 353 | flooredDiv : Int -> Float -> Int 354 | flooredDiv numerator denominator = 355 | floor (toFloat numerator / denominator) 356 | 357 | 358 | 359 | -- WEEKDAYS AND MONTHS 360 | 361 | 362 | {-| Represents a `Weekday` so that you can convert it to a `String` or `Int` 363 | however you please. For example, if you need the Japanese representation, you 364 | can say: 365 | 366 | toJapaneseWeekday : Weekday -> String 367 | toJapaneseWeekday weekday = 368 | case weekday of 369 | Mon -> "月" 370 | Tue -> "火" 371 | Wed -> "水" 372 | Thu -> "木" 373 | Fri -> "金" 374 | Sat -> "土" 375 | Sun -> "日" 376 | -} 377 | type Weekday = Mon | Tue | Wed | Thu | Fri | Sat | Sun 378 | 379 | 380 | {-| Represents a `Month` so that you can convert it to a `String` or `Int` 381 | however you please. For example, if you need the Danish representation, you 382 | can say: 383 | 384 | toDanishMonth : Month -> String 385 | toDanishMonth month = 386 | case month of 387 | Jan -> "januar" 388 | Feb -> "februar" 389 | Mar -> "marts" 390 | Apr -> "april" 391 | May -> "maj" 392 | Jun -> "juni" 393 | Jul -> "juli" 394 | Aug -> "august" 395 | Sep -> "september" 396 | Oct -> "oktober" 397 | Nov -> "november" 398 | Dec -> "december" 399 | -} 400 | type Month = Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec 401 | 402 | 403 | 404 | -- SUBSCRIPTIONS 405 | 406 | 407 | {-| Get the current time periodically. How often though? Well, you provide an 408 | interval in milliseconds (like `1000` for a second or `60 * 1000` for a minute 409 | or `60 * 60 * 1000` for an hour) and that is how often you get a new time! 410 | 411 | Check out [this example](https://elm-lang.org/examples/time) to see how to use 412 | it in an application. 413 | 414 | **This function is not for animation.** Use the [`onAnimationFrame`][af] 415 | function for that sort of thing! It syncs up with repaints and will end up 416 | being much smoother for any moving visuals. 417 | 418 | [af]: /packages/elm/browser/latest/Browser-Events#onAnimationFrame 419 | -} 420 | every : Float -> (Posix -> msg) -> Sub msg 421 | every interval tagger = 422 | subscription (Every interval tagger) 423 | 424 | 425 | type MySub msg = 426 | Every Float (Posix -> msg) 427 | 428 | 429 | subMap : (a -> b) -> MySub a -> MySub b 430 | subMap f (Every interval tagger) = 431 | Every interval (f << tagger) 432 | 433 | 434 | 435 | -- EFFECT MANAGER 436 | 437 | 438 | type alias State msg = 439 | { taggers : Taggers msg 440 | , processes : Processes 441 | } 442 | 443 | 444 | type alias Processes = 445 | Dict.Dict Float Platform.ProcessId 446 | 447 | 448 | type alias Taggers msg = 449 | Dict.Dict Float (List (Posix -> msg)) 450 | 451 | 452 | init : Task Never (State msg) 453 | init = 454 | Task.succeed (State Dict.empty Dict.empty) 455 | 456 | 457 | onEffects : Platform.Router msg Float -> List (MySub msg) -> State msg -> Task Never (State msg) 458 | onEffects router subs {processes} = 459 | let 460 | newTaggers = 461 | List.foldl addMySub Dict.empty subs 462 | 463 | leftStep interval taggers (spawns, existing, kills) = 464 | ( interval :: spawns, existing, kills ) 465 | 466 | bothStep interval taggers id (spawns, existing, kills) = 467 | ( spawns, Dict.insert interval id existing, kills ) 468 | 469 | rightStep _ id (spawns, existing, kills) = 470 | ( spawns, existing, Task.andThen (\_ -> kills) (Process.kill id) ) 471 | 472 | (spawnList, existingDict, killTask) = 473 | Dict.merge 474 | leftStep 475 | bothStep 476 | rightStep 477 | newTaggers 478 | processes 479 | ([], Dict.empty, Task.succeed ()) 480 | in 481 | killTask 482 | |> Task.andThen (\_ -> spawnHelp router spawnList existingDict) 483 | |> Task.andThen (\newProcesses -> Task.succeed (State newTaggers newProcesses)) 484 | 485 | 486 | addMySub : MySub msg -> Taggers msg -> Taggers msg 487 | addMySub (Every interval tagger) state = 488 | case Dict.get interval state of 489 | Nothing -> 490 | Dict.insert interval [tagger] state 491 | 492 | Just taggers -> 493 | Dict.insert interval (tagger :: taggers) state 494 | 495 | 496 | spawnHelp : Platform.Router msg Float -> List Float -> Processes -> Task.Task x Processes 497 | spawnHelp router intervals processes = 498 | case intervals of 499 | [] -> 500 | Task.succeed processes 501 | 502 | interval :: rest -> 503 | let 504 | spawnTimer = 505 | Process.spawn (setInterval interval (Platform.sendToSelf router interval)) 506 | 507 | spawnRest id = 508 | spawnHelp router rest (Dict.insert interval id processes) 509 | in 510 | spawnTimer 511 | |> Task.andThen spawnRest 512 | 513 | 514 | onSelfMsg : Platform.Router msg Float -> Float -> State msg -> Task Never (State msg) 515 | onSelfMsg router interval state = 516 | case Dict.get interval state.taggers of 517 | Nothing -> 518 | Task.succeed state 519 | 520 | Just taggers -> 521 | let 522 | tellTaggers time = 523 | Task.sequence (List.map (\tagger -> Platform.sendToApp router (tagger time)) taggers) 524 | in 525 | now 526 | |> Task.andThen tellTaggers 527 | |> Task.andThen (\_ -> Task.succeed state) 528 | 529 | 530 | setInterval : Float -> Task Never () -> Task x Never 531 | setInterval = 532 | Elm.Kernel.Time.setInterval 533 | 534 | 535 | 536 | -- FOR PACKAGE AUTHORS 537 | 538 | 539 | 540 | {-| **Intended for package authors.** 541 | 542 | The documentation of [`here`](#here) explains that it has certain accuracy 543 | limitations that block on adding new APIs to JavaScript. The `customZone` 544 | function is a stopgap that takes: 545 | 546 | 1. A default offset in minutes. So `Etc/GMT-5` is `customZone (-5 * 60) []` 547 | and `Etc/GMT+9` is `customZone (9 * 60) []`. 548 | 2. A list of exceptions containing their `start` time in "minutes since the Unix 549 | epoch" and their `offset` in "minutes from UTC" 550 | 551 | Human times will be based on the nearest `start`, falling back on the default 552 | offset if the time is older than all of the exceptions. 553 | 554 | When paired with `getZoneName`, this allows you to load the real IANA time zone 555 | database however you want: HTTP, cache, hardcode, etc. 556 | 557 | **Note:** If you use this, please share your work in an Elm community forum! 558 | I am sure others would like to hear about it, and more experience reports will 559 | help me and the any potential TC39 proposal. 560 | -} 561 | customZone : Int -> List { start : Int, offset : Int } -> Zone 562 | customZone = 563 | Zone 564 | 565 | 566 | {-| **Intended for package authors.** 567 | 568 | Use `Intl.DateTimeFormat().resolvedOptions().timeZone` to try to get names 569 | like `Europe/Moscow` or `America/Havana`. From there you can look it up in any 570 | IANA data you loaded yourself. 571 | -} 572 | getZoneName : Task x ZoneName 573 | getZoneName = 574 | Elm.Kernel.Time.getZoneName () 575 | 576 | 577 | {-| **Intended for package authors.** 578 | 579 | The `getZoneName` function relies on a JavaScript API that is not supported 580 | in all browsers yet, so it can return the following: 581 | 582 | -- in more recent browsers 583 | Name "Europe/Moscow" 584 | Name "America/Havana" 585 | 586 | -- in older browsers 587 | Offset 180 588 | Offset -300 589 | 590 | So if the real info is not available, it will tell you the current UTC offset 591 | in minutes, just like what `here` uses to make zones like `customZone -60 []`. 592 | -} 593 | type ZoneName 594 | = Name String 595 | | Offset Int 596 | --------------------------------------------------------------------------------