Marcus
Tox - Some structs and functions to work with dates, times, periods, and intervals
Hello,
I have released Tox 0.1.0, a little library for working with DateTime, NaiveDateTime, Date, and Time. It also adds Tox-Period and Tox-Interval. My motivation was to have some date-time calculation functions that work with any time zone database and different calendars. So you can use Tox with TimeZoneInfo, Tz, and Tzdata.
For the tests and examples, I use the Calendar.ISO and the calendars Cldr.Calendar.Coptic, Cldr.Calendar.Ethiopic, and Cldr.Calendar.Persian implemented by @kip.
Some examples:
add:
iex> datetime = DateTime.from_naive!(~N[2020-07-19 12:00:00], "Europe/Berlin")
#DateTime<2020-07-19 12:00:00+02:00 CEST Europe/Berlin>
iex> Tox.DateTime.add(datetime, year: 1, month: 1, day: 1, hour: 1, minute: 1)
#DateTime<2021-08-20 13:01:00+02:00 CEST Europe/Berlin>
iex> Tox.DateTime.add(datetime, year: -1, month: 1, day: 1, hour: 1, minute: 1)
#DateTime<2019-08-20 13:01:00+02:00 CEST Europe/Berlin>
iex> Tox.DateTime.add(datetime, week: 1)
#DateTime<2020-07-26 12:00:00+02:00 CEST Europe/Berlin>
iex> datetime = DateTime.from_naive!(~N[2020-07-19 12:00:00], "Europe/Berlin")
...> |> DateTime.convert!(Cldr.Calendar.Coptic)
#DateTime<1736-11-12 12:00:00+02:00 CEST Europe/Berlin Cldr.Calendar.Coptic>
iex> Tox.DateTime.add(datetime, month: 1)
#DateTime<1736-12-12 12:00:00+02:00 CEST Europe/Berlin Cldr.Calendar.Coptic>
iex> Tox.DateTime.add(datetime, month: 2)
#DateTime<1736-13-05 12:00:00+02:00 CEST Europe/Berlin Cldr.Calendar.Coptic>
iex> DateTime.from_naive(~N[2020-03-29 02:30:00], "Europe/Berlin")
{:gap, #DateTime<2020-03-29 01:59:59.999999+01:00 CET Europe/Berlin>,
#DateTime<2020-03-29 03:00:00+02:00 CEST Europe/Berlin>}
iex> datetime = DateTime.from_naive!(~N[2020-03-29 01:30:00], "Europe/Berlin")
#DateTime<2020-03-29 01:30:00+01:00 CET Europe/Berlin>
iex> Tox.DateTime.add(datetime, hour: 1)
#DateTime<2020-03-29 03:30:00+02:00 CEST Europe/Berlin>
beginning_of_, end_of_:
iex> ~N[2020-07-12 11:12:13]
...> |> DateTime.from_naive!("America/Winnipeg")
...> |> Tox.DateTime.beginning_of_year()
#DateTime<2020-01-01 00:00:00-06:00 CST America/Winnipeg>
iex> ~N[2020-07-12 11:12:13]
...> |> DateTime.from_naive!("Europe/Berlin")
...> |> Tox.DateTime.end_of_year()
#DateTime<2020-12-31 23:59:59.999999+01:00 CET Europe/Berlin>
# In 1994 the year ended one day earlier in the time zone Pacific/Kirimati.
iex> ~N[1994-07-12 11:12:13]
...> |> DateTime.from_naive!("Pacific/Kiritimati")
...> |> Tox.DateTime.end_of_year()
#DateTime<1994-12-30 23:59:59.999999-10:00 -10 Pacific/Kiritimati>
iex> datetime = DateTime.from_naive!(~N[2020-07-15 12:00:00], "Europe/Berlin")
#DateTime<2020-07-15 12:00:00+02:00 CEST Europe/Berlin>
iex> Tox.DateTime.end_of_week(datetime)
#DateTime<2020-07-19 23:59:59.999999+02:00 CEST Europe/Berlin>
iex> Tox.DateTime.end_of_day(datetime)
#DateTime<2020-07-15 23:59:59.999999+02:00 CEST Europe/Berlin>
iex> Tox.DateTime.beginning_of_week(datetime)
#DateTime<2020-07-13 00:00:00+02:00 CEST Europe/Berlin>
iex> Tox.DateTime.beginning_of_day(datetime)
#DateTime<2020-07-15 00:00:00+02:00 CEST Europe/Berlin>
iex> datetime = DateTime.from_naive!(~N[2020-07-19 12:00:00], "Europe/Berlin")
#DateTime<2020-07-19 12:00:00+02:00 CEST Europe/Berlin>
iex> datetime = DateTime.convert!(datetime, Cldr.Calendar.Coptic)
#DateTime<1736-11-12 12:00:00+02:00 CEST Europe/Berlin Cldr.Calendar.Coptic>
iex> Tox.DateTime.end_of_year(datetime)
#DateTime<1736-13-05 23:59:59.999999+02:00 CEST Europe/Berlin Cldr.Calendar.Coptic>
Tox.Period, Tox.Interval:
iex> now = DateTime.from_naive!(~N[2020-07-12 21:33:43], "America/New_York")
iex> period = Tox.Period.new!(month: 1)
#Tox.Period<P1M>
iex> interval = Tox.Interval.new!(Tox.DateTime.beginning_of_month(now), period)
#Tox.Interval<[2020-07-01T00:00:00-04:00/P1M[>
iex> Tox.Interval.contains?(interval, now)
true
iex> Tox.Interval.since_start(interval, now, :millisecond)
{:ok, 1028023000}
iex> Tox.Interval.until_ending(interval, now, :millisecond)
{:ok, 1650377000}
iex> Tox.Interval.until_ending(interval, Tox.DateTime.add(now, month: 1), :millisecond)
:error
Most Liked
josevalim
This looks great @Marcus!
We were recenetly having discussions about adding similar functionality directly to Elixir, so if you are interested in joining the discussion, we would love your $.02 on it: https://github.com/elixir-lang/elixir/pull/10199
josevalim
To add to that, most of Timex functionality has been included into Elixir itself at this point. The pending parts are:
- multiple timezone support - built into Elixir but requires tzdata
- datetime formatting - it will be in the next Elixir release (v1.11)
- datetime parsing - requires timex
- datetime shifting - requires timex or tox
So I like Tox exactly because it solves one problem well. Now we only need someone to cover or extract parsing from timex and we will have all functionality either in Elixir or available in a meal-piece fashion. 
LostKobrakai
This is mostly due to the fact that elixir doesn’t implement functionality, which doesn’t soundly cover the complexity involved, which takes effort and therefore time. E.g. what does month: 1 do when being added to 31. of January? Will it add 31 days, 30 days or result in 27. of February as the last day of the next month?
Given the constant movement I can however understand that it’s not easy to keep up to date at all times.
Marcus
@dimitarvp the example above show just Tox.DateTime, but Tox contains also Tox.NaiveDateTime, Tox.Date and Tox.Time all with here own add/2 function. The idea was not a Tox API but Tox as a namespace for some helpful modules and structs. It is the very first version an I am open to suggestions.
baldwindavid
@Marcus This looks great. Date/time handling is one of the concepts in Elixir that I’m never quite sure whether to use a library or whether it is available in core and I’m just not understanding it. It does seem like some of these things are evolving to get pulled into core though. I’ve previously been using the Timex library to fill in the gaps of necessary functionality. Would this be a direct alternative to that or is this different in its goals? It’s fine either way as it is nice to have multiple options.







