Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## v2.0.0 - 2026-08-??
- Move the timezone file loader to a separate package to make this usable
on web browsers.
- Update documentation to reflect the changes.

## v1.1.3 - 2026-08-08
- Add references to the zones gleam package with portable timezone data.
- Update documentation.
Expand Down
137 changes: 33 additions & 104 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,50 +3,64 @@
[![Package Version](https://img.shields.io/hexpm/v/tzif)](https://hex.pm/packages/tzif)
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/tzif/)

Time zone support for Gleam time using the IANA Time Zone Database.
This package loads the time zone database from the standard location
(`/usr/share/zoneinfo`) on MacOS and Linux computers. It includes a parser for
the Time Zone Information Format (TZif) or `tzfile` format, as well as utility
functions to convert a timestamp from the
[gleam_time](https://hexdocs.pm/gleam_time/) library into a date and time
of day in the given time zone.
Time zone support for Gleam time using the IANA Time Zone Database format.
This package includes a parser for the Time Zone Information Format (TZif) or
`tzfile` format, as well as utility functions to convert a timestamp from the
[gleam_time](https://hexdocs.pm/gleam_time/) library into a date and time of day
in the given time zone.

There are two ways to obtain the timezone data:
- The [zones](https://zones.hexdocs.pm/) package maintains up to date
time zone data in a native gleam package format. This is the recommended
method for code running in the browser, docker containers, and the Windows
operating system.

Add to your project with the command:

```
gleam add tzif@2 zones
```

- The [tzif_loader](https://tzif-loader.hexdocs.pm/) package will load the
operating system default time zone files from their standard location in
Linux and MacOS operating systems.

Add to your project with the command:

```
gleam add tzif@2 tzif_loader
```

> We could really do with a timezone database package with a
> fn(Timestamp, Zone) -> #(Date, TimeOfDay) function
>
> --- Louis Pilfold

Add to your project with the command:
```
gleam add tzif@1
```

# Using the Package
There are three modules in the `tzif` package:
- `tzif/database` has utilities for loading the IANA Time Zone database.
- `tzif/database` has utilities for managing the IANA Time Zone database.
- `tzif/tzcalendar` has utilities for converting a [gleam_time](https://gleam-time.hexdocs.pm/)
timestamp into date and time of day in a time zone.
- `tzif/parser` has functions and records for parsing TZif formatted data.

The most straightforward use would be to load the database from the default
location on the operating system, and then obtain a timestamp using the
[gleam_time](https://gleam-time.hexdocs.pm/) package, and convert that timestamp
into a time of day in a time zone using the IANA time zone name. An example
of that is shown in the code below.
Below is an example of code which loads the native time zone data from the
operating system using [tzif_loader](https://tzif-loader.hexdocs.pm/) and
converts the system time to a time of day in the America/New_York time zone.

```gleam
import gleam/int
import gleam/io
import gleam/string
import gleam/time/timestamp
import tzif/database
import tzif/loader
import tzif/tzcalendar

pub fn main() {
let now = timestamp.system_time()

// Load the database from the operating system
case database.load_from_os() {
case loader.load_from_os() {
Ok(db) -> {
case tzcalendar.to_time_and_zone(now, "America/New_York", db) {
Ok(time_and_zone) -> {
Expand All @@ -72,88 +86,3 @@ pub fn main() {
Error(Nil) -> io.println("No parsable TZif files found.")
}
```
If you are on windows and have installed the IANA Time Zone Database, or want
to use a custom version you can use the `database.load_from_path` function
instead of the `database.load_from_os` function to specify a path to your
database files.

# Installing the zoneinfo data files
Time zone information is frequently updated, therefore it makes sense to use the
package manager for your operating system to keep the time zone database up to
date. All common unix variants have time zone database packages and install the
time zone database files into the `/usr/share/zoneinfo` directory by default.

If, however, your system does not have time zone data installed, you can use
the [zones](https://zones.hexdocs.pm/) package to install a database of
timezone data as a gleam library and dependency. To use the zones timezone
data get the database using the `zones.database` function rather than
`database.load_from_os` or `database.load_from_path`.

## MacOS
The files should be included in your operating system by default. Check the
`/usr/share/zoneinfo` directory and see if they are present.

## Ubuntu/Debian Linux Systems
The APT package manager can be used to install the compiled TZif files with the
following command:

```
sudo apt install tzdata
```

### Debian based docker containers
Installing and configuring the time zone database on a Debian or Ubuntu based
docker container can be done by adding the following to your `Dockerfile`:

```
# Use an ARG for the timezone, with a default of UTC
ARG TIMEZONE=Etc/UTC

# Set the TZ environment variable and install tzdata
RUN apt-get update && \
export DEBIAN_FRONTEND=noninteractive && \
ln -fs /usr/share/zoneinfo/${TIMEZONE} /etc/localtime && \
apt-get install -y tzdata && \
dpkg-reconfigure --frontend noninteractive tzdata
```

## Alpine Linux Systems
The Alpine Package Keeper can install the time zone database using the command:

```
sudo apk add tzdata
```

### Alpine based docker containers
Installing and configuring the time zone database on an Alpine based docker
container can be done by adding the following to your `Dockerfile`:

```
# Use an ARG for the timezone, with a default of UTC
ARG TIMEZONE=Etc/UTC

# 1. Install the tzdata package
# 2. Copy the correct timezone file to /etc/localtime
# 3. Set the TZ environment variable to be used by applications
RUN apk add --no-cache tzdata && \
cp /usr/share/zoneinfo/${TIMEZONE} /etc/localtime && \
echo "${TIMEZONE}" > /etc/timezone
```

## Red Hat/Rocky/Alma Linux Systems
You can use the YUM package manager or DNF to install the time zone database
on Red Hat variants. To use YUM run the command:

```
sudo yum install tzdata
```

Similarly, using DNF:

```
sudo dnf install tzdata
```
## Windows
Microsoft Windows has a different mechanism for handling time zones, so we
recommend using the [zones](https://zones.hexdocs.pm/) portable gleam timezone
data library.
9 changes: 8 additions & 1 deletion examples/simple/gleam.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,14 @@ version = "1.0.0"

[dependencies]
gleam_stdlib = ">= 0.44.0 and < 2.0.0"
tzif = { path = "../../" }
tzif = {
git = "https://github.com/devries/timezone",
ref = "dfe650b9f761719b999511efbee7b902ed22c04f"
}
tzif_loader = {
git = "https://github.com/devries/tzif_loader",
ref = "2d64d20a4f6b1da6c48b3eaa5c49887f92885f0c"
}
gleam_time = ">= 1.4.0 and < 2.0.0"

[dev-dependencies]
Expand Down
6 changes: 4 additions & 2 deletions examples/simple/manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ packages = [
{ name = "gleam_time", version = "1.8.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "533D8723774D61AD4998324F5DD1DABDCDBFABAFB9E87CB5D03C6955448FC97D" },
{ name = "gleeunit", version = "1.11.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "EC31ABA74256AEA531EDF8169931D775BBB384FED0A8A1BDC4DD9354E3E21826" },
{ name = "simplifile", version = "2.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "A2727627B063E87351934C7F7F008F2D1FDB16F6DE0B8C79F9E46459CFC9C164" },
{ name = "tzif", version = "1.1.3", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib", "gleam_time", "simplifile"], source = "local", path = "../.." },
{ name = "tzif", version = "1.1.3", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib", "gleam_time", "simplifile"], source = "git", repo = "https://github.com/devries/timezone", commit = "dfe650b9f761719b999511efbee7b902ed22c04f" },
{ name = "tzif_loader", version = "1.0.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib", "gleam_time", "simplifile", "tzif"], source = "git", repo = "https://github.com/devries/tzif_loader", commit = "2d64d20a4f6b1da6c48b3eaa5c49887f92885f0c" },
]

[requirements]
gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" }
gleam_time = { version = ">= 1.4.0 and < 2.0.0" }
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
tzif = { path = "../../" }
tzif = { git = "https://github.com/devries/timezone", ref = "dfe650b9f761719b999511efbee7b902ed22c04f" }
tzif_loader = { git = "https://github.com/devries/tzif_loader", ref = "2d64d20a4f6b1da6c48b3eaa5c49887f92885f0c" }
9 changes: 8 additions & 1 deletion examples/time_now/gleam.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,14 @@ version = "1.0.0"

[dependencies]
gleam_stdlib = ">= 0.44.0 and < 2.0.0"
tzif = { path = "../../" }
tzif = {
git = "https://github.com/devries/timezone",
ref = "dfe650b9f761719b999511efbee7b902ed22c04f"
}
tzif_loader = {
git = "https://github.com/devries/tzif_loader",
ref = "2d64d20a4f6b1da6c48b3eaa5c49887f92885f0c"
}
gleam_time = ">= 1.4.0 and < 2.0.0"

[dev-dependencies]
Expand Down
6 changes: 4 additions & 2 deletions examples/time_now/manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ packages = [
{ name = "gleam_time", version = "1.8.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "533D8723774D61AD4998324F5DD1DABDCDBFABAFB9E87CB5D03C6955448FC97D" },
{ name = "gleeunit", version = "1.11.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "EC31ABA74256AEA531EDF8169931D775BBB384FED0A8A1BDC4DD9354E3E21826" },
{ name = "simplifile", version = "2.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "A2727627B063E87351934C7F7F008F2D1FDB16F6DE0B8C79F9E46459CFC9C164" },
{ name = "tzif", version = "1.1.3", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib", "gleam_time", "simplifile"], source = "local", path = "../.." },
{ name = "tzif", version = "1.1.3", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib", "gleam_time", "simplifile"], source = "git", repo = "https://github.com/devries/timezone", commit = "dfe650b9f761719b999511efbee7b902ed22c04f" },
{ name = "tzif_loader", version = "1.0.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib", "gleam_time", "simplifile", "tzif"], source = "git", repo = "https://github.com/devries/tzif_loader", commit = "2d64d20a4f6b1da6c48b3eaa5c49887f92885f0c" },
]

[requirements]
gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" }
gleam_time = { version = ">= 1.4.0 and < 2.0.0" }
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
tzif = { path = "../../" }
tzif = { git = "https://github.com/devries/timezone", ref = "dfe650b9f761719b999511efbee7b902ed22c04f" }
tzif_loader = { git = "https://github.com/devries/tzif_loader", ref = "2d64d20a4f6b1da6c48b3eaa5c49887f92885f0c" }
4 changes: 1 addition & 3 deletions gleam.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name = "tzif"
version = "1.1.3"
version = "2.0.0"

# Fill out these fields if you intend to generate HTML documentation or publish
# your project to the Hex package manager.
Expand All @@ -17,8 +17,6 @@ pages = [
[dependencies]
gleam_stdlib = ">= 0.44.0 and < 2.0.0"
gleam_time = ">= 1.4.0 and < 2.0.0"
simplifile = ">= 2.3.0 and < 3.0.0"
filepath = ">= 1.1.2 and < 2.0.0"

[dev-dependencies]
gleeunit = ">= 1.0.0 and < 2.0.0"
6 changes: 1 addition & 5 deletions manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,12 @@
# You should check this file into your source control repository.

packages = [
{ name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" },
{ name = "gleam_stdlib", version = "1.0.5", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "CEE5B6C076A85B45F60C585F4316C63EC8B7127C119D5738C3958A9C4D50404E" },
{ name = "gleam_time", version = "1.8.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "533D8723774D61AD4998324F5DD1DABDCDBFABAFB9E87CB5D03C6955448FC97D" },
{ name = "gleam_time", version = "1.10.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "56539216E4C4B1748714652AB38F0BD16B9101F61DB62769FDC7CD42A8E5E833" },
{ name = "gleeunit", version = "1.11.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "EC31ABA74256AEA531EDF8169931D775BBB384FED0A8A1BDC4DD9354E3E21826" },
{ name = "simplifile", version = "2.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "A2727627B063E87351934C7F7F008F2D1FDB16F6DE0B8C79F9E46459CFC9C164" },
]

[requirements]
filepath = { version = ">= 1.1.2 and < 2.0.0" }
gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" }
gleam_time = { version = ">= 1.4.0 and < 2.0.0" }
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
simplifile = { version = ">= 2.3.0 and < 3.0.0" }
55 changes: 0 additions & 55 deletions src/tzif/database.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@
//// from TZif formatted data you provide, or timezone data loaded from the
//// operating system.

import filepath
import gleam/dict
import gleam/list
import gleam/result
import gleam/string
import gleam/time/duration.{type Duration}
import gleam/time/timestamp
import simplifile
import tzif/parser

/// Time Zone Database record. This is typically created by
Expand All @@ -33,42 +31,6 @@ pub type TzDatabaseError {
InfoNotFound
}

/// Load time zone database from default operating system location
/// which is typically "/usr/share/zoneinfo". If no parsable TZif
/// files were found, returns `Error(Nil)`.
pub fn load_from_os() -> Result(TzDatabase, Nil) {
load_from_path("/usr/share/zoneinfo")
}

/// Load time zone database from provided directory. This is
/// useful if you have compiled your own version of the [IANA
/// time zone database](https://data.iana.org/time-zones/tz-link.html)
/// or they are not stored in the standard location. If no
/// parsable TZif files were found, returns `Error(Nil)`.
pub fn load_from_path(path: String) -> Result(TzDatabase, Nil) {
let parts = filepath.split(path)
let drop_number = list.length(parts)
use filenames <- result.try(
simplifile.get_files(path) |> result.replace_error(Nil),
)

let data =
filenames
|> list.map(process_tzfile(_, drop_number))
|> result.values

// If no parsable zone files were found return an Error rather than
// fail silently.
case list.length(data) {
0 -> Error(Nil)
_ ->
Ok(TzDatabase(
list.map(data, fn(v) { v.0 }) |> list.sort(string.compare),
dict.from_list(data),
))
}
}

/// Create new empty TzDatabase. This can be useful if you
/// will be loading TZif files using a different method, for
/// example over the internet, and wish to load them up into
Expand All @@ -93,23 +55,6 @@ pub fn add_tzfile(
TzDatabase(namelist, dict.insert(db.zone_data, zone_name, tzfile))
}

fn process_tzfile(
filename: String,
components_to_drop: Int,
) -> Result(#(String, parser.TzFile), Nil) {
let zone_name =
filepath.split(filename)
|> list.drop(components_to_drop)
|> list.fold("", filepath.join)

use tzdata <- result.try(
simplifile.read_bits(filename) |> result.replace_error(Nil),
)
use timeinfo <- result.try(parser.parse(tzdata) |> result.replace_error(Nil))

Ok(#(zone_name, timeinfo))
}

/// Get all list of all time zone names within the
/// time zone database.
pub fn get_available_timezones(db: TzDatabase) -> List(String) {
Expand Down
Loading
Loading