Core:time - Looking for best approach

One area of Odin’s core I’ve struggled with is the core:time library. I have so far been unable to figure out a straight forward way to get my local time formatted exactly how I want. I always end up with UTC. So far I’m doing the following, which works great, but I’ve been wondering for a while what the equivalent would look like using core:time. Could someone explain the best approach or is what I’m already doing the best way?

import "core:c/libc"

localtime :: proc(fmt: cstring, buf: []byte) -> (res: string) {
	now := libc.time(nil)
	lti := libc.localtime(&now)
	szt := libc.strftime(raw_data(buf[:]), len(buf), fmt, lti)
	return string(buf[:szt])
}

main :: proc() {
	buf: [32]byte
	mytime := localtime("My Time: %Y.%m.%d %I.%M %p", buf[:])
	fmt.println(mytime)
}

// output:
// My Time: 2026.03.14 07.23 PM

I think your solution is as succinct as I can think of trying to do it, without writing several lines of string building using rfc3339 as a starting point…

time.time_to_rfc3339() does include a UTC offset as an argument. Formatted as a string : YYYY-MM-DDTHH:MM:SSZ or YYYY-MM-DDTHH:MM:SS±HH:MM where ±HH:MM denotes your UTC offset.

Problem with strftime is it will crash the program if the format string is invalid, and there are no real practical ways to handle that. The best is to doing string validation, but that adds extra cost (especially if using regex), and open to missed edge-cases. The above works great for when I know the correct format I want to use, but not so great for more dynamic input. Plus, I think I read somewhere it will only recognize years up to 2039, which is not all that far away.

Guess I’ll have to keep banging away at core:time so I can take advantage of error handling.

I do not particularly need the string formatting part. That I can create myself, if can just figure out how to get local timezone, DST (active/not active), and a way to apply all that to time.now(). The time.time_to_rfc3339 procedure allows me to add a TZ offset to the resulting string, but it does not apply the offset to the time value. It is still UTC. I so far have not found a way to dynamically acquire local time through core:time.

After messing with this a bunch today, I stumbled across 2 ways to get local time. The key was using “local” for the reg string, which I did not find any documentation of, and no comments in the library defining what was possible. Just tried it, and whaazaa.

timezone.region_load("local", context.allocator)

I’m still very open to any advice on better approaches, but this is what I got so far.

1st way

region, region_ok := timezone.region_load("local", context.allocator)
defer timezone.region_destroy(region, context.allocator)
utc, utc_ok := time.time_to_datetime(time.now())
local_datetime, ldt_ok := timezone.datetime_to_tz(utc, region)

t, t_ok := time.datetime_to_time(local_datetime)
utc_offset := region.rrule.has_dst ? region.rrule.dst_offset / 60 : region.rrule.std_offset / 60
stime, stime_ok := time.time_to_rfc3339(t, int(utc_offset), allocator = context.allocator)
defer delete(stime, context.allocator)
fmt.println(stime)

2nd way

region, region_ok := timezone.region_load("local", context.allocator)
defer timezone.region_destroy(region, context.allocator)
utc_offset := region.rrule.has_dst ? region.rrule.dst_offset / 60 : region.rrule.std_offset / 60
now_local  := time.time_add(time.now(), time.Duration(utc_offset) * time.Minute)
stime, ok  := time.time_to_rfc3339(now_local, int(utc_offset), allocator = context.allocator)
defer delete(stime, context.allocator)
fmt.println(stime)

I then broke them down into the following procedures for future use in formatting what I want. Note the following comments for the local_datetime procedure below.

//	The returned date_time will have a reference to the returned tz_region
//	tz_region must stay allocated while using date_time
//	when done do timezone.region_destroy(tz_region, allocator)
local_datetime :: proc(allocator := context.allocator) -> (date_time: datetime.DateTime, tz_region: ^datetime.TZ_Region, ok: bool) {
	tz_region = timezone.region_load("local", allocator) or_return
	utc := time.time_to_datetime(time.now()) or_return
	date_time, ok = timezone.datetime_to_tz(utc, tz_region)
	return
}

local_timestamp_rfc3339 :: proc(allocator := context.allocator) -> (time_stamp: string, ok: bool) {
	now, utc_offset := local_now() or_return
	return time.time_to_rfc3339(now, int(utc_offset), allocator = allocator)
}

//	utc_offset is in minutes
local_now :: proc() -> (now: time.Time, utc_offset: i64, ok: bool) {
	region := timezone.region_load("local", context.allocator) or_return
	defer timezone.region_destroy(region, context.allocator)
	utc_offset = region.rrule.has_dst ? region.rrule.dst_offset / 60 : region.rrule.std_offset / 60
	now = time.time_add(time.now(), time.Duration(utc_offset) * time.Minute)
	return now, utc_offset, true
}
2 Likes