Skip to content

Commit fcd310d

Browse files
committed
refactoring and fix leap second bug
1 parent 46f4c28 commit fcd310d

File tree

12 files changed

+332
-29
lines changed

12 files changed

+332
-29
lines changed

README.md

Lines changed: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
# timezone
1+
# tzif
22

33
<!--#
44
[![Package Version](https://img.shields.io/hexpm/v/timezone)](https://hex.pm/packages/timezone)
55
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/timezone/)
66
-->
77

8-
Timezone support for Gleam time using the operating system's timezone database.
9-
This package loads the timezone database from the standard location
8+
Time zone support for Gleam time using the IANA Time Zone Database.
9+
This package loads the time zone database from the standard location
1010
(`/usr/share/zoneinfo`) on MacOS and Linux computers. It includes a parser for
1111
the Time Zone Information Format (TZif) or `tzfile` format, as well as utility
1212
functions to convert a timestamp from the
1313
[gleam_time](https://hexdocs.pm/gleam_time/) library into a date and time
14-
of day in the given timezone.
14+
of day in the given time zone.
1515

1616
> We could really do with a timezone database package with a
1717
> fn(Timestamp, Zone) -> #(Date, TimeOfDay) function
@@ -21,13 +21,61 @@ of day in the given timezone.
2121
To use, add the following entry in your `gleam.toml` file dependencies:
2222

2323
```
24-
timezone = { git = "[email protected]:devries/timezone.git", ref = "main" }
24+
tzif = { git = "[email protected]:devries/timezone.git", ref = "main" }
2525
```
26+
27+
# Using the Package
28+
The most straightforward use would be to load the database from the default
29+
location on the operating system, and then obtain a timestamp using the
30+
[gleam_time](https://hexdocs.pm/gleam_time/) package, and convert that timestamp
31+
into a time of day in a time zone using the IANA time zone name. An example
32+
of that is shown in the code below.
33+
34+
```gleam
35+
import gleam/int
36+
import gleam/io
37+
import gleam/string
38+
import gleam/time/timestamp
39+
import tzif/database
40+
import tzif/tzcalendar
41+
42+
pub fn main() {
43+
let now = timestamp.system_time()
44+
45+
// Load the database from the operating system
46+
let db = database.load_from_os()
47+
48+
case tzcalendar.get_time_and_zone(now, "America/New_York", db) {
49+
Ok(time_and_zone) -> {
50+
// Successfully converted time to the requested time zone
51+
io.println(
52+
int.to_string(time_and_zone.time_of_day.hours)
53+
|> string.pad_start(2, "0")
54+
<> ":"
55+
<> int.to_string(time_and_zone.time_of_day.minutes)
56+
|> string.pad_start(2, "0")
57+
<> ":"
58+
<> int.to_string(time_and_zone.time_of_day.seconds)
59+
|> string.pad_start(2, "0")
60+
<> " "
61+
<> time_and_zone.designation
62+
)
63+
}
64+
Error(database.ZoneNotFound) -> io.println("Time zone not found")
65+
Error(database.ProcessingError) -> io.println("Error processing time zone conversion")
66+
}
67+
}
68+
```
69+
If you are on windows and have installed the IANA Time Zone Database, or want
70+
to use a custom version you can use the `database.load_from_path` function
71+
instead of the `database.load_from_os` function to specify a path to your
72+
database files.
73+
2674
# Installing the zoneinfo data files
27-
Timezone information is frequently updated, therefore it makes sense to use the
28-
package manager for your operating system to keep the timezone database up to
29-
date. All common unix variants have timezone database packages and install the
30-
timezone database files into the `/usr/share/zoneinfo` directory by default.
75+
Time zone information is frequently updated, therefore it makes sense to use the
76+
package manager for your operating system to keep the time zone database up to
77+
date. All common unix variants have time zone database packages and install the
78+
time zone database files into the `/usr/share/zoneinfo` directory by default.
3179

3280
## MacOS
3381
The files should be included in your operating system by default. Check the
@@ -42,7 +90,7 @@ sudo apt install tzdata
4290
```
4391

4492
### Debian based docker containers
45-
Installing and configuring the timezone database on a Debian or Ubuntu based
93+
Installing and configuring the time zone database on a Debian or Ubuntu based
4694
docker container can be done by adding the following to your `Dockerfile`:
4795

4896
```
@@ -58,14 +106,14 @@ RUN apt-get update && \
58106
```
59107

60108
## Alpine Linux Systems
61-
The Alpine Package Keeper can install the timezone database using the command:
109+
The Alpine Package Keeper can install the time zone database using the command:
62110

63111
```
64112
sudo apk add tzdata
65113
```
66114

67115
### Alpine based docker containers
68-
Installing and configuring the timezone database on an Alpine based docker
116+
Installing and configuring the time zone database on an Alpine based docker
69117
container can be done by adding the following to your `Dockerfile`:
70118

71119
```
@@ -81,7 +129,7 @@ RUN apk add --no-cache tzdata && \
81129
```
82130

83131
## Red Hat/Rocky/Alma Linux Systems
84-
You can use the YUM package manager or DNF to install the timezone database
132+
You can use the YUM package manager or DNF to install the time zone database
85133
on Red Hat variants. To use YUM run the command:
86134

87135
```
@@ -94,4 +142,7 @@ Similarly, using DNF:
94142
sudo dnf install tzdata
95143
```
96144
## Windows
97-
At this time we have not tested the windows operating system.
145+
Microsoft Windows has a different mechanism for handling time zones, however
146+
you can install the IANA Time Zone Database by [downloading the latest
147+
version](https://www.iana.org/time-zones) and compiling the zone files using
148+
[the directions in the repository](https://data.iana.org/time-zones/tz-link.html).

examples/simple/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
*.beam
2+
*.ez
3+
/build
4+
erl_crash.dump

examples/simple/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# simple
2+
3+
[![Package Version](https://img.shields.io/hexpm/v/simple)](https://hex.pm/packages/simple)
4+
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/simple/)
5+
6+
```sh
7+
gleam add simple@1
8+
```
9+
```gleam
10+
import simple
11+
12+
pub fn main() -> Nil {
13+
// TODO: An example of the project in use
14+
}
15+
```
16+
17+
Further documentation can be found at <https://hexdocs.pm/simple>.
18+
19+
## Development
20+
21+
```sh
22+
gleam run # Run the project
23+
gleam test # Run the tests
24+
```

examples/simple/gleam.toml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name = "simple"
2+
version = "1.0.0"
3+
4+
# Fill out these fields if you intend to generate HTML documentation or publish
5+
# your project to the Hex package manager.
6+
#
7+
# description = ""
8+
# licences = ["Apache-2.0"]
9+
# repository = { type = "github", user = "", repo = "" }
10+
# links = [{ title = "Website", href = "" }]
11+
#
12+
# For a full reference of all the available options, you can have a look at
13+
# https://gleam.run/writing-gleam/gleam-toml/.
14+
15+
[dependencies]
16+
gleam_stdlib = ">= 0.44.0 and < 2.0.0"
17+
tzif = { path = "../../" }
18+
gleam_time = ">= 1.4.0 and < 2.0.0"
19+
20+
[dev-dependencies]
21+
gleeunit = ">= 1.0.0 and < 2.0.0"

examples/simple/manifest.toml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# This file was generated by Gleam
2+
# You typically do not need to edit this file
3+
4+
packages = [
5+
{ name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" },
6+
{ name = "gleam_stdlib", version = "0.63.2", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "962B25C667DA07F4CAB32001F44D3C41C1A89E58E3BBA54F183B482CF6122150" },
7+
{ name = "gleam_time", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "DCDDC040CE97DA3D2A925CDBBA08D8A78681139745754A83998641C8A3F6587E" },
8+
{ name = "gleeunit", version = "1.6.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "FDC68A8C492B1E9B429249062CD9BAC9B5538C6FBF584817205D0998C42E1DAC" },
9+
{ name = "simplifile", version = "2.3.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0A868DAC6063D9E983477981839810DC2E553285AB4588B87E3E9C96A7FB4CB4" },
10+
{ name = "tzif", version = "0.2.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib", "gleam_time", "simplifile"], source = "local", path = "../.." },
11+
]
12+
13+
[requirements]
14+
gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" }
15+
gleam_time = { version = ">= 1.4.0 and < 2.0.0" }
16+
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
17+
tzif = { path = "../../" }

examples/simple/src/simple.gleam

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import gleam/int
2+
import gleam/io
3+
import gleam/string
4+
import gleam/time/timestamp
5+
import tzif/database
6+
import tzif/tzcalendar
7+
8+
pub fn main() {
9+
let now = timestamp.system_time()
10+
11+
// Load the database from the operating system
12+
let db = database.load_from_os()
13+
14+
case tzcalendar.get_time_and_zone(now, "America/New_York", db) {
15+
Ok(time_and_zone) -> {
16+
// Successfully converted time to the requested time zone
17+
io.println(
18+
int.to_string(time_and_zone.time_of_day.hours)
19+
|> string.pad_start(2, "0")
20+
<> ":"
21+
<> int.to_string(time_and_zone.time_of_day.minutes)
22+
|> string.pad_start(2, "0")
23+
<> ":"
24+
<> int.to_string(time_and_zone.time_of_day.seconds)
25+
|> string.pad_start(2, "0")
26+
<> " "
27+
<> time_and_zone.designation,
28+
)
29+
}
30+
Error(database.ZoneNotFound) -> io.println("Time zone not found")
31+
Error(database.ProcessingError) ->
32+
io.println("Error processing time zone conversion")
33+
}
34+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import gleeunit
2+
3+
pub fn main() -> Nil {
4+
gleeunit.main()
5+
}
6+
7+
// gleeunit test functions end in `_test`
8+
pub fn hello_world_test() {
9+
let name = "Joe"
10+
let greeting = "Hello, " <> name <> "!"
11+
12+
assert greeting == "Hello, Joe!"
13+
}

src/tzif/database.gleam

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import gleam/time/timestamp
88
import simplifile
99
import tzif/tzparser
1010

11+
/// Time Zone Database record. This is typically created by
12+
/// loading from the operating system with the `load_from_os`
13+
/// function.
1114
pub opaque type TzDatabase {
1215
TzDatabase(
1316
zone_names: List(String),

src/tzif/tzcalendar.gleam

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
//// This module is for working with time zones and converting timestamps
22
//// from the `gleam/time` library into dates and times of day in a
3-
//// timezone.
3+
//// time zone.
44
////
55
//// This library makes use of the [IANA tz database](https://www.iana.org/time-zones)
66
//// which is generally already installed on computers.
7-
//// This library will search for timezone data in the [tzfile](https://www.man7.org/linux/man-pages/man5/tzfile.5.html)
7+
//// This library will search for timezone data in the TZif or [tzfile](https://www.man7.org/linux/man-pages/man5/tzfile.5.html)
88
//// file format. These are generally located in the `/usr/share/zoneinfo`
99
//// directory on posix systems, however if they are installed elsewhere the
10-
//// ZONEINFO environment variable can be set to the full path of the directory
10+
//// then they can be loaded ysung the full path of the directory
1111
//// containing the tz database files.
1212
////
1313
//// Time zone identifiers are generally of the form "Continent/City" for example
@@ -22,7 +22,10 @@ import gleam/time/duration.{type Duration}
2222
import gleam/time/timestamp.{type Timestamp}
2323
import tzif/database.{type TzDatabase, type TzDatabaseError}
2424

25-
/// Representation of time in a time zone
25+
/// Representation of a date and time of day in a particular time zone
26+
/// along with the offset from UTC, the zone designation (i.e. "UTC",
27+
/// "EST", "CEDT") and a boolean indicating if it is daylight savings
28+
/// time.
2629
pub type TimeAndZone {
2730
TimeAndZone(
2831
date: Date,

src/tzif/tzparser.gleam

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ pub type TzFileError {
2222

2323
/// Error parsing an integer
2424
IntegerParseError
25+
26+
/// Error Parsing leap second section
27+
LeapSecondParseError
2528
}
2629

2730
/// Header of the tzfile. This uses the same label names as
@@ -47,7 +50,7 @@ pub type TzFileFields {
4750
time_types: List(Int),
4851
ttinfos: List(TtInfo),
4952
designations: List(String),
50-
leapsecond_values: List(List(Int)),
53+
leapsecond_values: List(#(Int, Int)),
5154
standard_or_wall: List(Int),
5255
ut_or_local: List(Int),
5356
)
@@ -183,16 +186,13 @@ fn parse_section(
183186

184187
let designations = designation_tuples |> list.map(fn(tup) { tup.0 })
185188

186-
// Get leap second information
187-
use leapsecond_integers, remain <- parse_list(
189+
use leapsecond_values, remain <- parse_list(
188190
header.leapcnt,
189191
remain,
190192
[],
191-
integer_parser(integer_size),
193+
leap_parser(integer_size),
192194
)
193195

194-
let leapsecond_values = leapsecond_integers |> list.sized_chunk(2)
195-
196196
// Booleans to indicate if these are standard time or local time indicators
197197
use standard_wall_indicators, remain <- parse_list(
198198
header.ttisstdcnt,
@@ -276,6 +276,22 @@ fn unsigned_integer_parser(
276276
}
277277
}
278278

279+
fn leap_parser(
280+
bit_size: Int,
281+
) -> fn(BitArray, fn(#(Int, Int), BitArray) -> Result(a, TzFileError)) ->
282+
Result(a, TzFileError) {
283+
fn(bits: BitArray, next: fn(#(Int, Int), BitArray) -> Result(a, TzFileError)) {
284+
case bits {
285+
<<
286+
tt:signed-int-big-size(bit_size),
287+
leap:signed-int-big-size(32),
288+
rest:bits,
289+
>> -> next(#(tt, leap), rest)
290+
_ -> Error(LeapSecondParseError)
291+
}
292+
}
293+
}
294+
279295
fn parse_null_terminated_string(
280296
bits: BitArray,
281297
next: fn(String, BitArray) -> Result(a, TzFileError),

0 commit comments

Comments
 (0)