-
Notifications
You must be signed in to change notification settings - Fork 368
/
timezone_info.ex
127 lines (109 loc) · 5.37 KB
/
timezone_info.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
defmodule Timex.TimezoneInfo do
@moduledoc """
All relevant timezone information for a given period, i.e. Europe/Moscow on March 3rd, 2013
Notes:
- `full_name` is the name of the zone, but does not indicate anything about the current period (i.e. CST vs CDT)
- `abbreviation` is the abbreviated name for the zone in the current period, i.e. "America/Chicago" on 3/30/15 is "CDT"
- `offset_std` is the offset in minutes from standard time for this period
- `offset_utc` is the offset in minutes from UTC for this period
Spec:
- `day_of_week`: :sunday, :monday, :tuesday, etc
- `datetime`: {{year, month, day}, {hour, minute, second}}
- `from`: :min | {day_of_week, datetime}, when this zone starts
- `until`: :max | {day_of_week, datetime}, when this zone ends
"""
defstruct full_name: "Etc/UTC",
abbreviation: "UTC",
offset_std: 0,
offset_utc: 0,
from: :min,
until: :max
@valid_day_names [:sunday, :monday, :tuesday,
:wednesday, :thursday, :friday,
:saturday]
@max_seconds_in_day 60 * 60 * 24
@type day_of_week :: :sunday | :monday | :tuesday |
:wednesday | :thursday | :friday | :saturday
@type datetime :: {{non_neg_integer, 1..12, 1..31}, {0..24,0..59,0..60}}
@type offset :: -85399..85399
@type from_constraint :: :min | {day_of_week, datetime}
@type until_constraint :: :max | {day_of_week, datetime}
@type t :: %__MODULE__{
full_name: String.t,
abbreviation: String.t,
offset_std: offset,
offset_utc: offset,
from: from_constraint,
until: until_constraint
}
@doc """
Create a custom timezone if a built-in one does not meet your needs.
You must provide the name, abbreviation, offset from UTC, daylight savings time offset,
and the from/until reference points for when the zone takes effect and ends.
To clarify the two offsets, `offset_utc` is the absolute offset relative to UTC,
`offset_std` is the offset to apply to `offset_utc` which gives us the offset from UTC
during daylight savings time for this timezone. If DST does not apply for this zone, simply
set it to 0.
The from/until reference points must meet the following criteria:
- Be set to `:min` for from, or `:max` for until, which represent
"infinity" for the start/end of the zone period.
- OR, be a tuple of {day_of_week, datetime}, where:
- `day_of_week` is an atom like `:sunday`
- `datetime` is an Erlang datetime tuple, e.g. `{{2016,10,8},{2,0,0}}`
*IMPORTANT*: Offsets are in seconds, not minutes, if you do not ensure they
are in the correct unit, runtime errors or incorrect results are probable.
## Examples
iex> #{__MODULE__}.create("Etc/Test", "TST", 120*60, 0, :min, :max)
%TimezoneInfo{full_name: "Etc/Test", abbreviation: "TST", offset_std: 7200, offset_utc: 0, from: :min, until: :max}
...> #{__MODULE__}.create("Etc/Test", "TST", 24*60*60, 0, :min, :max)
{:error, "invalid timezone offset '86400'"}
"""
@spec create(String.t, String.t, offset, offset, from_constraint, until_constraint) :: __MODULE__.t | {:error, String.t}
def create(name, abbr, offset_utc, offset_std, from, until) do
%__MODULE__{
full_name: name,
abbreviation: abbr,
offset_std: offset_std,
offset_utc: offset_utc,
from: from || :min,
until: until || :max
} |> validate_and_return()
end
defp validate_and_return(%__MODULE__{} = tz) do
with true <- is_valid_name(tz.full_name),
true <- is_valid_name(tz.abbreviation),
true <- is_valid_offset(tz.offset_std),
true <- is_valid_offset(tz.offset_utc),
true <- is_valid_from_constraint(tz.from),
true <- is_valid_until_constraint(tz.until),
do: tz
end
defp is_valid_name(name) when is_binary(name), do: true
defp is_valid_name(name), do: {:error, "invalid timezone name '#{inspect name}'!"}
defp is_valid_offset(offset) when is_integer(offset) and (offset < @max_seconds_in_day and offset > -@max_seconds_in_day),
do: true
defp is_valid_offset(offset), do: {:error, "invalid timezone offset '#{inspect offset}'"}
defp is_valid_from_constraint(:min), do: true
defp is_valid_from_constraint(:max), do: {:error, ":max is not a valid from constraint for timezones"}
defp is_valid_from_constraint(c), do: is_valid_constraint(c)
defp is_valid_until_constraint(:min), do: {:error, ":min is not a valid until constraint for timezones"}
defp is_valid_until_constraint(:max), do: true
defp is_valid_until_constraint(c), do: is_valid_constraint(c)
defp is_valid_constraint({day_of_week, {{y,m,d},{h,mm,s}}} = datetime) when day_of_week in @valid_day_names do
cond do
:calendar.valid_date({y,m,d}) ->
valid_hour = h >= 1 and h <= 24
valid_min = mm >= 0 and mm <= 59
valid_sec = s >= 0 and s <= 59
cond do
valid_hour && valid_min && valid_sec ->
true
:else ->
{:error, "invalid datetime constraint for timezone: #{inspect datetime} (invalid time)"}
end
:else ->
{:error, "invalid datetime constraint for timezone: #{inspect datetime} (invalid date)"}
end
end
defp is_valid_constraint(c), do: {:error, "'#{inspect c}' is not a valid constraint for timezones"}
end