├── test ├── test_helper.exs └── kronos_test.exs ├── lib ├── kronos │ └── infix.ex └── kronos.ex ├── mix.lock ├── .gitignore ├── LICENSE ├── config └── config.exs ├── README.md └── mix.exs /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /lib/kronos/infix.ex: -------------------------------------------------------------------------------- 1 | defmodule Kronos.Infix do 2 | 3 | @moduledoc """ 4 | This module is a shortcut to import `Mizur.Infix` functions. 5 | """ 6 | 7 | defmacro __using__(opts) do 8 | quote do 9 | use(Mizur.Infix, unquote(opts)) 10 | end 11 | end 12 | 13 | end 14 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"dialyxir": {:hex, :dialyxir, "0.5.0", "5bc543f9c28ecd51b99cc1a685a3c2a1a93216990347f259406a910cf048d1d7", [:mix], []}, 2 | "earmark": {:hex, :earmark, "1.2.0", "bf1ce17aea43ab62f6943b97bd6e3dc032ce45d4f787504e3adf738e54b42f3a", [:mix], []}, 3 | "ex_doc": {:hex, :ex_doc, "0.15.1", "d5f9d588fd802152516fccfdb96d6073753f77314fcfee892b15b6724ca0d596", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, optional: false]}]}, 4 | "mizur": {:hex, :mizur, "1.0.1", "e78361e46248264a29624b40882d6e10b66b380aaef52dc7ace20d07b64b0acf", [:mix], []}} 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Xavier Van de Woestyne 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :kronos, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:kronos, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kronos 2 | 3 | Kronos is a library to facilitate simple arithmetic operations between timestamps. 4 | At [Dernier Cri](https://derniercri.io) (my ex-company), 5 | we often have to handle DateTime. Kronos was designed to avoid having 6 | to constantly convert DateTime into timestamps and vice-verça. 7 | 8 | If you are looking for a complete library of time and date management, 9 | Kronos is (maybe) not the ideal solution, and 10 | I recommend [Timex](https://github.com/bitwalker/timex)! 11 | 12 | Kronos relies on [Mizur](https://github.com/xvw/mizur) to decorate numerical 13 | values of typing information. 14 | 15 | The library supports Mizur arithmetic operations, Timestamps collisions, 16 | inclusions between timestamps intervals (via Mizur.Range), and truncation of 17 | timestamps. I invite you to read the full documentation for more information! 18 | 19 | [https://hexdocs.pm/kronos](https://hexdocs.pm/kronos) 20 | 21 | ## Small examples 22 | 23 | ```elixir 24 | import Kronos 25 | use Kronos.Infix # Same of Mizur.Infix 26 | 27 | {:ok, t} = new({2010, 12, 20}, {0, 0, 0}) 28 | # You can use timestamp or DateTime.t as parameter for Kronos.new 29 | 30 | r = t + ~t(2)day + ~t(3)hour + ~t(10)minute + ~t(13)second 31 | IO.puts Kronos.to_string(r) # will print "2010-12-22 03:10:13Z" 32 | ``` 33 | 34 | 35 | 36 | ## Installation 37 | 38 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 39 | by adding `kronos` to your list of dependencies in `mix.exs`: 40 | 41 | ```elixir 42 | def deps do 43 | [{:kronos, "~> 1.0.0"}] 44 | end 45 | ``` 46 | 47 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) 48 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can 49 | be found at [https://hexdocs.pm/kronos](https://hexdocs.pm/kronos). 50 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Kronos.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :kronos, 7 | version: "1.0.0", 8 | elixir: "~> 1.4", 9 | build_embedded: Mix.env == :prod, 10 | start_permanent: Mix.env == :prod, 11 | name: "Kronos", 12 | source_url: "https://github.com/xvw/kronos", 13 | homepage_url: "https://github.com/xvw/kronos/doc", 14 | deps: deps(), 15 | package: package(), 16 | description: description(), 17 | docs: docs() 18 | ] 19 | end 20 | 21 | defp description do 22 | """ 23 | Kronos is a library to facilitate simple arithmetic operations between timestamps. 24 | This library is based on Mizur to type values. 25 | """ 26 | end 27 | 28 | defp package do 29 | [ 30 | name: :kronos, 31 | files: ["lib", "mix.exs", "README*", "LICENSE*"], 32 | maintainers: ["Xavier Van de Woestyne"], 33 | licenses: ["MIT"], 34 | links: %{"GitHub" => "https://github.com/xvw/kronos", 35 | "Docs" => "http://xvw.github.io/kronos/doc/readme.html"}] 36 | end 37 | 38 | # configuration of the documentation 39 | def docs do 40 | [ 41 | main: "readme", 42 | extras: [ 43 | "README.md" 44 | ] 45 | ] 46 | end 47 | 48 | # Configuration for the OTP application 49 | # 50 | # Type "mix help compile.app" for more information 51 | def application do 52 | # Specify extra applications you'll use from Erlang/Elixir 53 | [extra_applications: [:logger]] 54 | end 55 | 56 | # Dependencies can be Hex packages: 57 | # 58 | # {:my_dep, "~> 0.3.0"} 59 | # 60 | # Or git/path repositories: 61 | # 62 | # {:my_dep, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 63 | # 64 | # Type "mix help deps" for more examples and options 65 | defp deps do 66 | [ 67 | {:mizur, "~> 1.0.1"}, 68 | {:dialyxir, "~> 0.5", only: [:dev], runtime: false}, 69 | {:ex_doc, "~> 0.14", only: :dev, runtime: false} 70 | ] 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /test/kronos_test.exs: -------------------------------------------------------------------------------- 1 | defmodule KronosTest do 2 | use ExUnit.Case 3 | doctest Kronos 4 | 5 | # Timestamp for : 2017/04/25 22:21:15 6 | @ts 1493158875 7 | @dt DateTime.from_unix!(@ts) 8 | 9 | 10 | def mock(:day, year, month, day) do 11 | Kronos.new!({year, month, day}, {0, 0, 0}) 12 | end 13 | 14 | def mock(:day, year, month, day, h, m, s) do 15 | Kronos.new!({year, month, day}, {h, m, s}) 16 | end 17 | 18 | def mock(:duration, year1, year2) do 19 | a = Kronos.new!({year1, 1, 1}, {0, 0, 0}) 20 | b = Kronos.new!({year2, 1, 1}, {0, 0, 0}) 21 | Kronos.laps(from: a, to: b) 22 | end 23 | 24 | test "Kronos.t creation" do 25 | {:ok, t} = Kronos.new(@ts) 26 | assert Kronos.to_datetime!(t) == @dt 27 | end 28 | 29 | test "Kronos.t creation failure" do 30 | r = Kronos.new({-2, 10, 10},{-3, 12, 68}) 31 | assert {:error, :invalid_date} == r 32 | end 33 | 34 | test "Kronos.t arithmetics operations" do 35 | day = @dt.day + 2 36 | min = @dt.minute + 12 37 | 38 | use Kronos.Infix, only: [+: 2] 39 | t = Kronos.new!(@ts) + Kronos.day(2) + Kronos.minute(12) 40 | 41 | m = 42 | @dt 43 | |> Map.put(:day, day) 44 | |> Map.put(:minute, min) 45 | 46 | assert Kronos.to_datetime!(t) == m 47 | 48 | end 49 | 50 | test "for truncate" do 51 | a = mock(:day, 2017, 5, 6, 20, 12, 10) 52 | b = mock(:day, 2017, 2, 13, 17, 25, 59) 53 | c = mock(:day, 2002, 9, 15, 1, 2, 3) 54 | d = mock(:day, 1970, 1, 1) 55 | 56 | 57 | assert Kronos.truncate(a, at: Kronos.minute) == mock(:day, 2017, 5, 6, 20, 12, 0) 58 | assert Kronos.truncate(a, at: Kronos.hour) == mock(:day, 2017, 5, 6, 20, 0, 0) 59 | assert Kronos.truncate(a, at: Kronos.day) == mock(:day, 2017, 5, 6) 60 | assert Kronos.truncate(a, at: Kronos.second) == a 61 | 62 | assert Kronos.truncate(b, at: Kronos.minute) == mock(:day, 2017, 2, 13, 17, 25, 0) 63 | assert Kronos.truncate(b, at: Kronos.second) == b 64 | assert Kronos.truncate(b, at: Kronos.hour) == mock(:day, 2017, 2, 13, 17, 0, 0) 65 | assert Kronos.truncate(b, at: Kronos.day) == mock(:day, 2017, 2, 13) 66 | 67 | assert Kronos.truncate(c, at: Kronos.minute) == mock(:day, 2002, 9, 15, 1, 2, 0) 68 | assert Kronos.truncate(c, at: Kronos.second) == c 69 | assert Kronos.truncate(c, at: Kronos.hour) == mock(:day, 2002, 9, 15, 1, 0, 0) 70 | assert Kronos.truncate(c, at: Kronos.day) == mock(:day, 2002, 9, 15) 71 | 72 | assert Kronos.truncate(d, at: Kronos.minute) == d 73 | assert Kronos.truncate(d, at: Kronos.second) == d 74 | assert Kronos.truncate(d, at: Kronos.hour) == d 75 | assert Kronos.truncate(d, at: Kronos.day) == d 76 | 77 | end 78 | 79 | 80 | test "for truncate negative ts" do 81 | a = mock(:day, 1908, 5, 6, 20, 12, 10) 82 | b = mock(:day, 1907, 2, 13, 17, 25, 59) 83 | c = mock(:day, 1958, 9, 15, 1, 2, 3) 84 | d = mock(:day, 1965, 1, 1) 85 | 86 | 87 | assert Kronos.truncate(a, at: Kronos.minute) == mock(:day, 1908, 5, 6, 20, 12, 0) 88 | assert Kronos.truncate(a, at: Kronos.hour) == mock(:day, 1908, 5, 6, 20, 0, 0) 89 | assert Kronos.truncate(a, at: Kronos.day) == mock(:day, 1908, 5, 6) 90 | 91 | assert Kronos.truncate(b, at: Kronos.minute) == mock(:day, 1907, 2, 13, 17, 25, 0) 92 | assert Kronos.truncate(b, at: Kronos.hour) == mock(:day, 1907, 2, 13, 17, 0, 0) 93 | assert Kronos.truncate(b, at: Kronos.day) == mock(:day, 1907, 2, 13) 94 | 95 | assert Kronos.truncate(c, at: Kronos.minute) == mock(:day, 1958, 9, 15, 1, 2, 0) 96 | assert Kronos.truncate(c, at: Kronos.hour) == mock(:day, 1958, 9, 15, 1, 0, 0) 97 | assert Kronos.truncate(c, at: Kronos.day) == mock(:day, 1958, 9, 15) 98 | 99 | 100 | assert Kronos.truncate(a, at: Kronos.second) == a 101 | assert Kronos.truncate(b, at: Kronos.second) == b 102 | assert Kronos.truncate(c, at: Kronos.second) == c 103 | 104 | assert Kronos.truncate(d, at: Kronos.second) == d 105 | assert Kronos.truncate(d, at: Kronos.minute) == d 106 | assert Kronos.truncate(d, at: Kronos.hour) == d 107 | assert Kronos.truncate(d, at: Kronos.day) == d 108 | 109 | 110 | end 111 | 112 | test "truncate for week 1!" do 113 | a = mock(:day, 2017, 10, 4, 22, 12, 32) 114 | assert Kronos.truncate(a, at: Kronos.week) == mock(:day, 2017, 10, 2) 115 | assert Kronos.truncate(a, at: Kronos.week(start: :sun)) == mock(:day, 2017, 10, 1) 116 | assert Kronos.truncate(a, at: Kronos.week(start: :sat)) == mock(:day, 2017, 9, 30) 117 | end 118 | 119 | test "truncate for week 2!" do 120 | a = mock(:day, 2017, 1, 6, 23, 59, 59) 121 | assert Kronos.truncate(a, at: Kronos.week) == mock(:day, 2017, 1, 2) 122 | assert Kronos.truncate(a, at: Kronos.week(start: :sun)) == mock(:day, 2017, 1, 1) 123 | assert Kronos.truncate(a, at: Kronos.week(start: :sat)) == mock(:day, 2016, 12, 31) 124 | end 125 | 126 | test "truncate for week 3!" do 127 | a = mock(:day, 2017, 1, 6) 128 | assert Kronos.truncate(a, at: Kronos.week) == mock(:day, 2017, 1, 2) 129 | assert Kronos.truncate(a, at: Kronos.week(start: :sun)) == mock(:day, 2017, 1, 1) 130 | assert Kronos.truncate(a, at: Kronos.week(start: :sat)) == mock(:day, 2016, 12, 31) 131 | end 132 | 133 | test "truncate for week 4!" do 134 | a = mock(:day, 1951, 12, 6) 135 | assert Kronos.truncate(a, at: Kronos.week) == mock(:day, 1951, 12, 3) 136 | assert Kronos.truncate(a, at: Kronos.week(start: :sun)) == mock(:day, 1951, 12, 2) 137 | assert Kronos.truncate(a, at: Kronos.week(start: :sat)) == mock(:day, 1951, 12, 1) 138 | end 139 | 140 | 141 | 142 | test "For Day of week" do 143 | 144 | a = mock(:day, 2017, 5, 6) # :sat 145 | b = mock(:day, 2017, 2, 13) # :mon 146 | c = mock(:day, 2002, 9, 15) # :sun 147 | d = mock(:day, 1989, 11, 3) # :fri 148 | e = mock(:day, 2026, 12, 9) # :wed 149 | f = mock(:day, 2029, 6, 5) # :tue 150 | g = mock(:day, 1970, 1, 1) # :thu 151 | 152 | assert Kronos.day_of_week(a) == :sat 153 | assert Kronos.day_of_week(b) == :mon 154 | assert Kronos.day_of_week(c) == :sun 155 | assert Kronos.day_of_week(d) == :fri 156 | assert Kronos.day_of_week(e) == :wed 157 | assert Kronos.day_of_week(f) == :tue 158 | assert Kronos.day_of_week(g) == :thu 159 | end 160 | 161 | test "Days with negative timestamp" do 162 | 163 | a = mock(:day, 1907, 5, 4) 164 | b = mock(:day, 1907, 6, 10) 165 | c = mock(:day, 1907, 2, 17) 166 | d = mock(:day, 1907, 3, 22) 167 | e = mock(:day, 1911, 5, 10) 168 | f = mock(:day, 1911, 6, 6) 169 | g = mock(:day, 1911, 4, 27) 170 | 171 | assert Kronos.day_of_week(a) == :sat 172 | assert Kronos.day_of_week(b) == :mon 173 | assert Kronos.day_of_week(c) == :sun 174 | assert Kronos.day_of_week(d) == :fri 175 | assert Kronos.day_of_week(e) == :wed 176 | assert Kronos.day_of_week(f) == :tue 177 | assert Kronos.day_of_week(g) == :thu 178 | end 179 | 180 | 181 | test "next & pred 1" do 182 | a = mock(:day, 2016, 2, 4) 183 | af = Kronos.next(Kronos.week(start: :mon), of: a) 184 | bf = Kronos.pred(Kronos.week(start: :sun), of: a) 185 | assert af == mock(:day, 2016, 2, 8) 186 | assert bf == mock(:day, 2016, 1, 24) 187 | end 188 | 189 | test "next 1" do 190 | a = mock(:day, 2016, 2, 4, 13, 28, 47) 191 | af = Kronos.next(Kronos.day(), of: a) 192 | assert af == mock(:day, 2016, 2, 5) 193 | end 194 | 195 | test "pred 1" do 196 | a = mock(:day, 2016, 2, 4, 13, 28, 47) 197 | af = Kronos.pred(Kronos.day(), of: a) 198 | assert af == mock(:day, 2016, 2, 3) 199 | end 200 | 201 | test "next 2" do 202 | a = mock(:day, 1951, 12, 6) 203 | af = Kronos.next(Kronos.week, of: a) 204 | ab = Kronos.next(Kronos.week(start: :sun), of: a) 205 | assert af == mock(:day, 1951, 12, 10) 206 | assert ab == mock(:day, 1951, 12, 9) 207 | end 208 | 209 | test "pred 2" do 210 | a = mock(:day, 1951, 12, 6) 211 | af = Kronos.pred(Kronos.week, of: a) 212 | ab = Kronos.pred(Kronos.week(start: :sun), of: a) 213 | assert af == mock(:day, 1951, 11, 26) 214 | assert ab == mock(:day, 1951, 11, 25) 215 | end 216 | 217 | 218 | 219 | 220 | end 221 | -------------------------------------------------------------------------------- /lib/kronos.ex: -------------------------------------------------------------------------------- 1 | defmodule Kronos do 2 | 3 | @moduledoc """ 4 | Kronos is a tool to facilitate the manipulation of dates (via Timestamps). 5 | This library use the seconds as a reference. 6 | 7 | 8 | iex> import Kronos 9 | ...> use Kronos.Infix 10 | ...> {:ok, t} = new({2010, 12, 20}, {0, 0, 0}) 11 | ...> r = t + ~t(2)day + ~t(3)hour + ~t(10)minute + ~t(13)second 12 | ...> Kronos.to_string(r) 13 | "2010-12-22 03:10:13Z" 14 | 15 | The measures references are : 16 | 17 | - `Kronos.second` 18 | - `Kronos.minute` 19 | - `Kronos.second` 20 | - `Kronos.fifteen_minutes` 21 | - `Kronos.half_hour` 22 | - `Kronos.hour` 23 | - `Kronos.half_day` 24 | - `Kronos.day` 25 | - `Kronos.week` 26 | - `Kronos.month` **This measure is an approximation (30 days)** 27 | - `Kronos.year` **This measure is an approximation (365 days)** 28 | 29 | 30 | ## Unsafe values 31 | 32 | `Kronos.month` and `Kronos.year` are unsafe, they represents a "approximation" 33 | of a month or a year. You should never use it with `Kronos.truncate/2`, 34 | `Kronos.next/2` and `Kronos.pred/2` (and of course as a `precision flag`). 35 | 36 | 37 | """ 38 | 39 | @first_day_of_week 3 40 | @days_of_week [ 41 | :mon, 42 | :tue, 43 | :wed, 44 | :thu, 45 | :fri, 46 | :sat, 47 | :sun 48 | ] 49 | 50 | 51 | @typedoc """ 52 | This type represents a typed timestamp 53 | """ 54 | @type t :: Mizur.typed_value 55 | 56 | @typedoc """ 57 | This type represents a specific week type 58 | """ 59 | @type week_t :: {t, day_of_week} 60 | 61 | @typedoc """ 62 | This type represents a metric_type 63 | """ 64 | @type metric :: Mizur.metric_type | week_t 65 | 66 | @typedoc """ 67 | This type represents a range between two timestamp 68 | """ 69 | @type duration :: Mizur.Range.range 70 | 71 | @typedoc """ 72 | This type represents a triplet of non negative values 73 | """ 74 | @type non_neg_triplet :: { 75 | non_neg_integer, 76 | non_neg_integer, 77 | non_neg_integer 78 | } 79 | 80 | @typedoc """ 81 | This type represents a couple date-time 82 | """ 83 | @type datetime_t :: { 84 | non_neg_triplet, 85 | non_neg_triplet 86 | } 87 | 88 | @typedoc """ 89 | This type represents a failable result 90 | """ 91 | @type result :: {:ok, t} | {:error, atom} 92 | 93 | @typedoc """ 94 | This type represents the day of the week 95 | """ 96 | @type day_of_week :: 97 | :mon 98 | | :tue 99 | | :wed 100 | | :thu 101 | | :fri 102 | | :sat 103 | | :sun 104 | 105 | # Internals helpers 106 | 107 | def one({mod, unit, _, _, _}), do: apply(mod, unit, [1]) 108 | def one({t, _}), do: one(t) 109 | 110 | defp int_to_dow(i), do: Enum.at(@days_of_week, i) 111 | defp dow_to_int(d) do 112 | Enum.find_index( 113 | @days_of_week, 114 | fn(x) -> x == d end 115 | ) 116 | end 117 | 118 | defp modulo(a, b) do 119 | cond do 120 | a >= 0 -> rem(a, b) 121 | true -> b - 1 - rem(-a-1, b) 122 | end 123 | end 124 | 125 | defp second?({_, :second, _, _, _}), do: true 126 | defp second?(_), do: false 127 | 128 | defp simple_week?({_, :week, _, _, _}), do: true 129 | defp simple_week?(_), do: false 130 | 131 | # Definition of the Metric-System 132 | 133 | @doc """ 134 | Monkeypatch to truncate `Kronos.t`. 135 | """ 136 | @spec week([start: day_of_week]) :: week_t 137 | def week(start: day), do: {week(), day} 138 | 139 | use Mizur.System 140 | 141 | type second 142 | 143 | type minute = 60 * second 144 | type fifteen_minutes = 15 * 60 * second 145 | type half_hour = 30 * 60 * second 146 | type hour = 60 * 60 * second 147 | type half_day = 12 * 60 * 60 * second 148 | type day = 24 * 60 * 60 * second 149 | type week = 7 * 24 * 60 * 60 * second 150 | 151 | type month = 30 * 24 * 60 * 60 * second 152 | type year = 365 * 24 * 60 * 60 * second 153 | 154 | 155 | @doc """ 156 | Convert a `Kronos.t` into a string, use the 157 | `DateTime` inspect. 158 | """ 159 | @spec to_string(t) :: String.t 160 | def to_string(value) do 161 | case to_datetime(value) do 162 | {:error, reason} -> "Invalid[#{reason}]" 163 | {:ok, datetime} -> "#{datetime}" 164 | end 165 | end 166 | 167 | @doc """ 168 | Returns if the given year is a leap year. 169 | 170 | iex> Kronos.leap_year?(2004) 171 | true 172 | 173 | iex> Kronos.leap_year?(2017) 174 | false 175 | 176 | """ 177 | @spec leap_year?(non_neg_integer) :: boolean 178 | def leap_year?(year) do 179 | rem(year, 4) === 0 180 | and (rem(year, 100) > 0 or rem(year, 400) === 0) 181 | end 182 | 183 | @doc """ 184 | Returns a `Kronos.t` with the number of days in a month, 185 | the month is referenced by `year` and `month (non neg integer)`. 186 | 187 | iex> Kronos.days_in(2004, 2) 188 | Kronos.day(29) 189 | 190 | iex> Kronos.days_in(2005, 2) 191 | Kronos.day(28) 192 | 193 | iex> Kronos.days_in(2005, 1) 194 | Kronos.day(31) 195 | 196 | iex> Kronos.days_in(2001, 4) 197 | Kronos.day(30) 198 | 199 | """ 200 | @spec days_in(non_neg_integer, 1..12) :: t 201 | def days_in(year, month), do: day(aux_days_in(year, month)) 202 | defp aux_days_in(year, 2), do: (if (leap_year?(year)), do: 29, else: 28) 203 | defp aux_days_in(_, month) when month in [4, 6, 9, 11], do: 30 204 | defp aux_days_in(_, _), do: 31 205 | 206 | @doc """ 207 | Converts an integer (timestamp) to a `Kronos.result` 208 | """ 209 | @spec new(integer) :: result 210 | def new(timestamp) when is_integer(timestamp) do 211 | case DateTime.from_unix(timestamp) do 212 | {:ok, _datetime} -> {:ok, second(timestamp)} 213 | {:error, reason } -> {:error, reason} 214 | end 215 | end 216 | 217 | @doc """ 218 | Converts an erlang datetime representation to a `Kronos.result` 219 | """ 220 | @spec new(datetime_t) :: result 221 | def new({{_, _, _}, {_, _, _}} = erl_tuple) do 222 | case NaiveDateTime.from_erl(erl_tuple) do 223 | {:error, reason1} -> {:error, reason1} 224 | {:ok, naive} -> 225 | {:ok, result} = DateTime.from_naive(naive, "Etc/UTC") 226 | {:ok, from_datetime(result)} 227 | end 228 | end 229 | 230 | @doc """ 231 | Converts two tuple (date, time) to a `Kronos.result` 232 | """ 233 | @spec new(non_neg_triplet, non_neg_triplet) :: result 234 | def new({_, _, _} = date, {_, _, _} = time) do 235 | new({date, time}) 236 | end 237 | 238 | @doc """ 239 | Same of `Kronos.new/1` but raise an `ArgumentError` if the 240 | timestamp creation failed. 241 | """ 242 | @spec new!(integer | datetime_t) :: t 243 | def new!(input) do 244 | case new(input) do 245 | {:ok, result} -> result 246 | {:error, reason} -> 247 | raise ArgumentError, message: "Invalid argument, #{reason}" 248 | end 249 | end 250 | 251 | @doc """ 252 | Same of `Kronos.new/2` but raise an `ArgumentError` if the 253 | timestamp creation failed. 254 | """ 255 | @spec new!(non_neg_triplet, non_neg_triplet) :: t 256 | def new!(date, time), do: new!({date, time}) 257 | 258 | 259 | @doc """ 260 | Creates a duration between two `Kronos.t`. This duration 261 | is a `Mizur.Range.range`. 262 | 263 | iex> a = Kronos.new!(1) 264 | ...> b = Kronos.new!(100) 265 | ...> Kronos.laps(a, b) 266 | Mizur.Range.new(Kronos.new!(1), Kronos.new!(100)) 267 | """ 268 | @spec laps(t, t) :: duration 269 | def laps(a, b), do: Mizur.Range.new(a, b) 270 | 271 | @doc """ 272 | Check if a `Kronos.t` is include into a `Kronos.duration`. 273 | 274 | iex> duration = KronosTest.mock(:duration, 2017, 2018) 275 | ...> a = KronosTest.mock(:day, 2015, 12, 10) 276 | ...> b = KronosTest.mock(:day, 2017, 5, 10) 277 | ...> {Kronos.include?(a, in: duration), Kronos.include?(b, in: duration)} 278 | {false, true} 279 | """ 280 | @spec include?(t, [in: duration]) :: boolean 281 | def include?(a, in: b), do: Mizur.Range.include?(a, in: b) 282 | 283 | @doc """ 284 | Checks that two durations have an intersection. 285 | 286 | iex> durationA = KronosTest.mock(:duration, 2016, 2018) 287 | ...> durationB = KronosTest.mock(:duration, 2017, 2019) 288 | ...> Kronos.overlap?(durationA, with: durationB) 289 | true 290 | """ 291 | @spec overlap?(duration, [with: duration]) :: boolean 292 | def overlap?(a, with: b), do: Mizur.Range.overlap?(a, b) 293 | 294 | @doc """ 295 | Creates a duration between two `Kronos.t`. This duration 296 | is a `Mizur.Range.range`. 297 | 298 | iex> a = Kronos.new!(1) 299 | ...> b = Kronos.new!(100) 300 | ...> [from: a, to: b] |> Kronos.laps 301 | Mizur.Range.new(Kronos.new!(1), Kronos.new!(100)) 302 | """ 303 | @spec laps([from: t, to: t]) :: duration 304 | def laps(from: a, to: b), do: laps(a, b) 305 | 306 | 307 | @doc """ 308 | Returns the current timestamp (in a `Kronos.t`) 309 | """ 310 | @spec now() :: t 311 | def now() do 312 | DateTime.utc_now 313 | |> DateTime.to_unix(:second) 314 | |> second() 315 | end 316 | 317 | @doc """ 318 | Returns the wrapped values (into a `Kronos.t`) as an 319 | integer in `second`. This function is mainly used to convert 320 | `Kronos.t` to` DateTime.t`. 321 | 322 | iex> x = Kronos.new!(2000) 323 | ...> Kronos.to_integer(x) 324 | 2000 325 | 326 | """ 327 | @spec to_integer(t) :: integer 328 | def to_integer(timestamp) do 329 | elt = Mizur.from(timestamp, to: second()) 330 | round(Mizur.unwrap(elt)) 331 | end 332 | 333 | @doc """ 334 | Converts a `Kronos.t` to a `DateTime.t`, the result is wrapped 335 | into `{:ok, value}` or `{:error, reason}`. 336 | 337 | iex> ts = 1493119897 338 | ...> a = Kronos.new!(ts) 339 | ...> b = DateTime.from_unix(1493119897) 340 | ...> Kronos.to_datetime(a) == b 341 | true 342 | """ 343 | @spec to_datetime(t) :: {:ok, DateTime.t} | {:error, atom} 344 | def to_datetime(timestamp) do 345 | timestamp 346 | |> to_integer() 347 | |> DateTime.from_unix(:second) 348 | end 349 | 350 | @doc """ 351 | Converts a `Kronos.t` to a `DateTime.t`. Raise an `ArgumentError` if 352 | the timestamp is not valid. 353 | 354 | iex> ts = 1493119897 355 | ...> a = Kronos.new!(ts) 356 | ...> b = DateTime.from_unix!(1493119897) 357 | ...> Kronos.to_datetime!(a) == b 358 | true 359 | """ 360 | @spec to_datetime!(t) :: DateTime.t 361 | def to_datetime!(timestamp) do 362 | timestamp 363 | |>to_integer() 364 | |> DateTime.from_unix!(:second) 365 | end 366 | 367 | @doc """ 368 | Converts a `DateTime.t` into a `Kronos.t` 369 | """ 370 | @spec from_datetime(DateTime.t) :: t 371 | def from_datetime(datetime) do 372 | datetime 373 | |> DateTime.to_unix(:second) 374 | |> second() 375 | end 376 | 377 | @doc """ 378 | `Kronos.after?(a, b)` check if `a` is later in time than `b`. 379 | 380 | iex> {a, b} = {Kronos.new!(2), Kronos.new!(1)} 381 | ...> Kronos.after?(a, b) 382 | true 383 | 384 | You can specify a `precision`, to ignore minutes, hours or days. 385 | (By passing a `precision`, both parameters will be truncated via 386 | `Kronos.truncate/2`). 387 | """ 388 | @spec after?(t, t, metric) :: boolean 389 | def after?(a, b, precision \\ second()) do 390 | use Mizur.Infix, only: [>: 2] 391 | truncate(a, at: precision) > truncate(b, at: precision) 392 | end 393 | 394 | @doc """ 395 | `Kronos.before?(a, b)` check if `a` is earlier in time than `b`. 396 | 397 | iex> {a, b} = {Kronos.new!(2), Kronos.new!(1)} 398 | ...> Kronos.before?(b, a) 399 | true 400 | 401 | You can specify a `precision`, to ignore minutes, hours or days. 402 | (By passing a `precision`, both parameters will be truncated via 403 | `Kronos.truncate/2`). 404 | """ 405 | @spec before?(t, t, metric) :: boolean 406 | def before?(a, b, precision \\ second()) do 407 | use Mizur.Infix, only: [<: 2] 408 | truncate(a, at: precision) < truncate(b, at: precision) 409 | end 410 | 411 | 412 | @doc """ 413 | `Kronos.equivalent?(a, b)` check if `a` is at the same moment of `b`. 414 | 415 | iex> {a, b} = {Kronos.new!(2), Kronos.new!(1)} 416 | ...> Kronos.equivalent?(b, a, Kronos.hour()) 417 | true 418 | 419 | You can specify a `precision`, to ignore minutes, hours or days. 420 | (By passing a `precision`, both parameters will be truncated via 421 | `Kronos.truncate/2`). 422 | """ 423 | @spec equivalent?(t, t, metric) :: boolean 424 | def equivalent?(a, b, precision \\ second()) do 425 | use Mizur.Infix, only: [==: 2] 426 | truncate(a, at: precision) == truncate(b, at: precision) 427 | end 428 | 429 | 430 | @doc """ 431 | Rounds the given timestamp (`timestamp`) to the given type (`at`). 432 | 433 | iex> ts = Kronos.new!({2017, 10, 24}, {23, 12, 07}) 434 | ...> Kronos.truncate(ts, at: Kronos.hour()) 435 | Kronos.new!({2017, 10, 24}, {23, 0, 0}) 436 | 437 | For example : 438 | - truncate of 2017/10/24 23:12:07 at `minute` gives : 2017/10/24 23:12:00 439 | - truncate of 2017/10/24 23:12:07 at `hour` gives : 2017/10/24 23:00:00 440 | - truncate of 2017/10/24 23:12:07 at `day` gives : 2017/10/24 00:00:00 441 | 442 | """ 443 | @spec truncate(t, [at: metric]) :: t 444 | 445 | def truncate(timestamp, at: {_, dow}) do 446 | ts = truncate(timestamp, at: day()) 447 | f = modulo(day_of_week_internal(ts) - dow_to_int(dow), 7) 448 | Mizur.sub(ts, day(f)) 449 | end 450 | 451 | def truncate({base, _} = timestamp, at: t) do 452 | cond do 453 | second?(t) -> timestamp 454 | simple_week?(t) -> truncate(timestamp, at: week(start: :mon)) 455 | true -> 456 | seconds = to_integer(timestamp) 457 | factor = to_integer(one(t)) 458 | (seconds - modulo(seconds, factor)) 459 | |> second() 460 | |> Mizur.from(to: base) 461 | end 462 | end 463 | 464 | 465 | @doc """ 466 | Returns the difference (always positive) between to members 467 | of a duration. 468 | 469 | iex> duration = KronosTest.mock(:duration, 2017, 2018) 470 | ...> Mizur.from((Kronos.diff(duration)), to: Kronos.day) 471 | Kronos.day(365) 472 | """ 473 | @spec diff(duration) :: t 474 | def diff(duration) do 475 | {a, b} = Mizur.Range.sort(duration) 476 | Mizur.sub(b, a) 477 | end 478 | 479 | @doc """ 480 | Jump to the next value of a `type`. For example 481 | `next(Kronos.day, of: Kronos.new({2017, 10, 10}, {22, 12, 12}))` give the 482 | date : `2017-10-11, 0:0:0`. 483 | 484 | iex> t = KronosTest.mock(:day, 2017, 10, 10) 485 | ...> Kronos.next(Kronos.day, of: t) 486 | KronosTest.mock(:day, 2017, 10, 11) 487 | """ 488 | @spec next(metric, [of: t]) :: t 489 | def next(t, of: ts) do 490 | Mizur.add(ts, one(t)) 491 | |> truncate(at: t) 492 | end 493 | 494 | 495 | @doc """ 496 | Jump to the pred value of a `type`. For example 497 | `next(Kronos.day, of: Kronos.new({2017, 10, 10}, {22, 12, 12}))` give the 498 | date : `2017-10-09, 0:0:0`. 499 | 500 | iex> t = KronosTest.mock(:day, 2017, 10, 10) 501 | ...> Kronos.pred(Kronos.day, of: t) 502 | KronosTest.mock(:day, 2017, 10, 9) 503 | """ 504 | @spec pred(metric, [of: t]) :: t 505 | def pred(t, of: ts) do 506 | Mizur.sub(ts, one(t)) 507 | |> truncate(at: t) 508 | end 509 | 510 | 511 | @doc """ 512 | Returns the day of the week from a `Kronos.t`. 513 | 0 for Monday, 6 for Sunday. 514 | 515 | iex> a = KronosTest.mock(:day, 1970, 1, 1, 12, 10, 11) 516 | ...> Kronos.day_of_week_internal(a) 517 | 3 518 | 519 | iex> a = KronosTest.mock(:day, 2017, 4, 29, 0, 3, 11) 520 | ...> Kronos.day_of_week_internal(a) 521 | 5 522 | 523 | 524 | """ 525 | @spec day_of_week_internal(t) :: 0..6 526 | def day_of_week_internal(ts) do 527 | ts 528 | |> truncate(at: day()) 529 | |> Mizur.from(to: day()) 530 | |> Mizur.unwrap() 531 | |> round() 532 | |> Kernel.+(@first_day_of_week) 533 | |> modulo(7) 534 | end 535 | 536 | @doc """ 537 | Returns the day of the week from a `Kronos.t`. 538 | 0 for Monday, 6 for Sunday. 539 | 540 | iex> a = KronosTest.mock(:day, 1970, 1, 1, 12, 10, 11) 541 | ...> Kronos.day_of_week(a) 542 | :thu 543 | 544 | iex> a = KronosTest.mock(:day, 2017, 4, 29, 0, 3, 11) 545 | ...> Kronos.day_of_week(a) 546 | :sat 547 | 548 | 549 | """ 550 | @spec day_of_week(t) :: day_of_week 551 | def day_of_week(ts) do 552 | day_of_week_internal(ts) 553 | |> int_to_dow() 554 | end 555 | 556 | 557 | @doc """ 558 | Returns the seconds (relatives) of a timestamp. 559 | 560 | iex> a = KronosTest.mock(:day, 2017, 10, 10, 23, 45, 53) 561 | ...> Kronos.seconds_of(a) 562 | 53 563 | 564 | iex> a = KronosTest.mock(:day, 1903, 10, 10, 13, 22, 7) 565 | ...> Kronos.seconds_of(a) 566 | 7 567 | """ 568 | @spec seconds_of(t) :: integer 569 | def seconds_of(timestamp) do 570 | modulo(to_integer(timestamp), 60) 571 | end 572 | 573 | @doc """ 574 | Returns the minutes (relatives) of a timestamp. 575 | 576 | iex> a = KronosTest.mock(:day, 2017, 10, 10, 23, 45, 53) 577 | ...> Kronos.minutes_of(a) 578 | 45 579 | 580 | iex> a = KronosTest.mock(:day, 1903, 10, 10, 13, 22, 7) 581 | ...> Kronos.minutes_of(a) 582 | 22 583 | """ 584 | @spec minutes_of(t) :: integer 585 | def minutes_of(timestamp) do 586 | timestamp 587 | |> truncate(at: minute()) 588 | |> to_integer() 589 | |> Kernel.div(60) 590 | |> modulo(60) 591 | end 592 | 593 | @doc """ 594 | Returns the hours (relatives) of a timestamp. 595 | 596 | iex> a = KronosTest.mock(:day, 2017, 10, 10, 23, 45, 53) 597 | ...> Kronos.hours_of(a) 598 | 23 599 | 600 | iex> a = KronosTest.mock(:day, 1903, 10, 10, 13, 22, 7) 601 | ...> Kronos.hours_of(a) 602 | 13 603 | """ 604 | @spec hours_of(t) :: integer 605 | def hours_of(timestamp) do 606 | timestamp 607 | |> truncate(at: hour()) 608 | |> to_integer() 609 | |> Kernel.div(3600) 610 | |> modulo(24) 611 | end 612 | 613 | 614 | end 615 | --------------------------------------------------------------------------------