Adding timezone to utc epoch seconds

for a hobby project I have utc seconds since unix epoch and want to have a Ptime.t * int with correct timezone offset in a given timezone. Daylight saving makes it nasty. Currently I hack something with index (timedesc.index) and Ptime / Erratique but my approach doesn’t feel quite right. Also I want to be lean on dependencies and avoid Jane street’s core.

let tz = "Europe/Zurich" |> Timedesc.Time_zone.make_exn
and of_rfc3339 (str : string) : (Ptime.t * int) =
    match str |> Ptime.of_rfc3339 with
    | Ok (t, Some tz_s, _) -> (t, tz_s)
    | _ -> (Ptime.min, 0)
in
let of_epoch utc_s =
    utc_s |> Timedesc.of_timestamp_float_s ~tz_of_date_time:tz
    |> Option.get |> Timedesc.to_rfc3339 |> Option.get |> of_rfc3339
in
...

How would I git rid of the rfc3339 string conversion steps?

P.S.: the real code is mro/internet-radio-recorder: 📻 Record internet radio broadcasts and turn them into RSS feeds (podcasts) with a raspberry pi a.k.a. raspi (or any other debian-like box) - pages/stations/b2/app/broadcast-scrape-cmd/lib/br.ml at develop - internet-radio-recorder - Codeberg.org

I don’t understand what you’re asking. A time in seconds-since-the-epoch does not come with a timezone. Maybe you’re asking “if I have an epoch-time, and a time-zone, can I deduce whether that timezone is correct (or needs to be adjusted for DST) ?”

If you can get a Timedesc.t, there appears to be a tz function on that type. Maybe this helps, maybe not – I’m just kibitzing.

Happy hacking!

@Chet_Murthy I need the offset for a timezone on a given day honouring daylight saving – say Europe/Zurich and day x (as utc epoch). Then I can use Ptime, because when finally serialising (to_rfc3339) I have to add that information.

Ho do I get the timezone offset for a timezone + utc epoch?

thanks @Chet_Murthy for bearing with me – but I don’t find offset_from_utc inside timere/timedesc.ml at main · daypack-dev/timere · GitHub

Also I’d prefer to avoid timedesc and get the offset from the zone + day somehow. Maybe @darrenldl knows by heart.

Right, it’s in date_time.ml, which is include-ed in timedesc.ml

1 Like

“Offset for time zone on a given day honouring daylight saving” is unfortunately not well defined, so really can’t give you a good answer before I finish reading what you’re trying to do.

Also, if you want to get a Ptime.t, you can also use the builtin conversion functions in Utils: Timedesc (timedesc.Timedesc)

1 Like

Right, so you scrapped a seconds since unix epoch string, converted it into float, and want to process it into a Broadcast.timestamp (which is defined as Ptime.t * int) that makes sense in the Europe/Zurich time zone when read by a human, namely with an offset that makes sense with respect to whether DST is in effect, I’m guessing?

Suppose you obtain said float as x, you can do

module Td = Timedesc

let tz = Td.Time_zone.make_exn "Europe/Zurich" in
let time = Td.of_timestamp_float_s_exn ~tz_of_date_time:tz x in
let offset = match Td.offset_from_utc time with
  | `Single x -> Td.Span.get_s x
  | `Ambiguous _ -> failwith "Unexpected case"
in
let ptime = Td.to_timestamp_single time time
            |> Td.Utils.ptime_of_timestamp
            |> Option.get
in
(ptime, offset)

I’ll add I’m curious about the insistence on using Ptime as the base unit of handling time - if you’re using only Ptime then that makes sense since Ptime carries way fewer dependencies, but you’re using Timedesc anyway.

2 Likes

Thanks a lot, that looks great (was struggling with the ambiguous cases).

I may pass around a record with 2 timestamps and may factor some basic operations into a (opam?) package – so I want the visible surface be as lean as possible.

Indeed, the timere build dependency is there anyway because of Time_zone.

finally - avoid rfc3339 intermediate when scraping broadcast start and end times, · c384e63ad6 - internet-radio-recorder - Codeberg.org

Do you mind expanding on your use case? (feel free to ignore it, I’m just curious)

What is the user of such a package supposed to do with the offset? Once you have a timestampt + offset, it’s very tempting to start using it. E.g. applying the same offset to a different timestamp, adding some seconds to the timestamp, etc. All of that could potentially lead to an invalid offset because we crossed a DST boundary.

Why isn’t the solution to have timestamp + zone id, which allows you to retrieve a local time whenever you need to do so?

2 Likes

@beajeanm the broad use-case is handling radio broadcast meta data e.g. persisting and deserialising, computing overlaps, schedule recordings, bundle recordings into RSS feeds etc. for a private DIY internet radio recorder.

Some of those use-cases is tackled with OCaml, currently mostly the scraping of the meta-data from the radio stations websites (at the heart a per-station Unix filter in OCaml). Cross-station meta data storage is flat files (xml) in webroot of a webserver (no DB involved). E.g. http://rec.mro.name/stations/b2/2021/10/03/1905.xml

Times in above xml are rfc3339. Including a timezone numerical offset. So that’s the minimum information needed (for storage).

Source is the station’s broadcast page e.g. Zündfunk extra | Bayern 2 | Radio | BR.de which has UTC epoch times (hidden in page source). And the station is in a known, fixed timezone.

So when scraping the station B2 (more to come), I finally have to turn a utc epoch + timezone into rfc3339. But I use an internal time representation Ptime * int for less conversions when calculating durations etc.

As it is internal, it could be Timedescs, but for now I am with Ptime * int and see where this leads to.

The package I mentioned might be the cross-station parts like what is a broadcast without the scraping that is specific to the station(s). That package will not provide functions about times, zones etc.

I vaguely recall Timedesc.Span being comparable to what Ptime offers in terms of duration handling, if that’s what you’re after.