const std = @import("std"); const zeit = @import("zeit"); const TimeZoneOffsets = @import("location/timezone_offsets.zig"); const Coordinates = @import("Coordinates.zig"); const c_double = f64; // We don't use @cImport here because sunriset.c has a main() function // Instead we declare the functions we need directly extern fn __sunriset__(year: c_int, month: c_int, day: c_int, lon: c_double, lat: c_double, altit: c_double, upper_limb: c_int, rise: *c_double, set: *c_double) c_int; extern fn __daylen__(year: c_int, month: c_int, day: c_int, lon: c_double, lat: c_double, altit: c_double, upper_limb: c_int) c_double; /// We will copy these macros in from the c file as proper functions // This macro computes the length of the day, from sunrise to sunset. // Sunrise/set is considered to occur when the Sun's upper limb is // 35 arc minutes below the horizon (this accounts for the refraction // of the Earth's atmosphere). fn day_length(year: c_int, month: c_int, day: c_int, lon: c_double, lat: c_double) c_double { return __daylen__(year, month, day, lon, lat, -35.0 / 60.0, 1); } // This macro computes the length of the day, including civil twilight. // Civil twilight starts/ends when the Sun's center is 6 degrees below // the horizon. fn day_civil_twilight_length(year: c_int, month: c_int, day: c_int, lon: c_double, lat: c_double) c_double { return __daylen__(year, month, day, lon, lat, -6.0, 0); } // This macro computes the length of the day, incl. nautical twilight. // Nautical twilight starts/ends when the Sun's center is 12 degrees // below the horizon. fn day_nautical_twilight_length(year: c_int, month: c_int, day: c_int, lon: c_double, lat: c_double) c_double { return __daylen__(year, month, day, lon, lat, -12.0, 0); } // This macro computes the length of the day, incl. astronomical twilight. // Astronomical twilight starts/ends when the Sun's center is 18 degrees // below the horizon. fn day_astronomical_twilight_length(year: c_int, month: c_int, day: c_int, lon: c_double, lat: c_double) c_double { return __daylen__(year, month, day, lon, lat, -18.0, 0); } // This macro computes times for sunrise/sunset. // Sunrise/set is considered to occur when the Sun's upper limb is // 35 arc minutes below the horizon (this accounts for the refraction // of the Earth's atmosphere). fn sun_rise_set(year: c_int, month: c_int, day: c_int, lon: c_double, lat: c_double, rise: *c_double, set: *c_double) c_int { return __sunriset__(year, month, day, lon, lat, -35.0 / 60.0, 1, rise, set); } // This macro computes the start and end times of civil twilight. // Civil twilight starts/ends when the Sun's center is 6 degrees below // the horizon. fn civil_twilight(year: c_int, month: c_int, day: c_int, lon: c_double, lat: c_double, start: *c_double, end: *c_double) c_int { return __sunriset__(year, month, day, lon, lat, -6.0, 0, start, end); } // This macro computes the start and end times of nautical twilight. // Nautical twilight starts/ends when the Sun's center is 12 degrees // below the horizon. fn nautical_twilight(year: c_int, month: c_int, day: c_int, lon: c_double, lat: c_double, start: *c_double, end: *c_double) c_int { return __sunriset__(year, month, day, lon, lat, -12.0, 0, start, end); } // This macro computes the start and end times of astronomical twilight. // Astronomical twilight starts/ends when the Sun's center is 18 degrees // below the horizon. fn astronomical_twilight(year: c_int, month: c_int, day: c_int, lon: c_double, lat: c_double, start: *c_double, end: *c_double) c_int { return __sunriset__(year, month, day, lon, lat, -18.0, 0, start, end); } const Astronomical = @This(); dawn: Time, // Hours in UTC (civil twilight start) sunrise: Time, // Hours in UTC zenith: Time, // Hours in UTC (solar noon) sunset: Time, // Hours in UTC dusk: Time, // Hours in UTC (civil twilight end) pub const Time = struct { year: i32, month: zeit.Month, day: u5, hour: u5, minute: u6, offset: i32 = 0, pub fn init(sunriset_time: f64, year: i32, month: zeit.Month, day: u5) Time { const h: u8 = @intFromFloat(@floor(sunriset_time)); return .{ .year = year, .month = month, .day = day, .hour = @intCast(h), .minute = @intFromFloat(@floor((sunriset_time - @as(f64, @floatFromInt(h))) * 60.0)), }; } pub fn adjustTimeToLocal(self: Time, coords: Coordinates) !Time { const ztime: zeit.Time = .{ .year = self.year, .month = self.month, .day = self.day, .hour = self.hour, .minute = self.minute, }; const original = ztime.instant(); const offset = TimeZoneOffsets.getTimezoneOffset(coords); const new = if (offset >= 0) try original.add(.{ .minutes = @abs(offset) }) else try original.subtract(.{ .minutes = @abs(offset) }); const new_ztime = new.time(); return .{ .year = new_ztime.year, .month = new_ztime.month, .day = new_ztime.day, .hour = new_ztime.hour, .minute = new_ztime.minute, .offset = offset, }; } pub fn format(self: Time, writer: *std.Io.Writer) std.Io.Writer.Error!void { try writer.print("{d:0>2}:{d:0>2}", .{ self.hour, self.minute }); } }; /// Returns all times in UTC /// /// calculates astronomical data for lat/long and a timestamp using /// sunriset.c (http://www.stjarnhimlen.se/comp/sunriset.c) /// /// Note: year,month,date = calendar date, 1801-2099 only. pub fn init(latitude: f64, longitude: f64, timestamp: i64) Astronomical { const instant = zeit.instant(.{ .source = .{ .unix_timestamp = timestamp } }) catch @panic("This can't happen"); const time = instant.time(); const year: c_int = @intCast(time.year); const month: c_int = @intFromEnum(time.month); const day: c_int = @intCast(time.day); std.log.debug("year: {}, month: {}, day: {}", .{ year, month, day }); var sunrise: f64 = 0; var sunset: f64 = 0; var dawn: f64 = 0; var dusk: f64 = 0; // Notes from the c file itself: // Eastern longitude positive, Western longitude negative // Northern latitude positive, Southern latitude negative // // The longitude value IS critical in this function! // // altit = the altitude which the Sun should cross // Set to -35/60 degrees for rise/set, -6 degrees // for civil, -12 degrees for nautical and -18 // degrees for astronomical twilight. // upper_limb: non-zero -> upper limb, zero -> center // Set to non-zero (e.g. 1) when computing rise/set // times, and to zero when computing start/end of // twilight. // *rise = where to store the rise time // *set = where to store the set time // Both times are relative to the specified altitude, // and thus this function can be used to compute // various twilight times, as well as rise/set times // Return value: 0 = sun rises/sets this day, times stored at // *trise and *tset. // +1 = sun above the specified "horizon" 24 hours. // *trise set to time when the sun is at south, // minus 12 hours while *tset is set to the south // time plus 12 hours. "Day" length = 24 hours // -1 = sun is below the specified "horizon" 24 hours // "Day" length = 0 hours, *trise and *tset are // both set to the time when the sun is at south. // Get sunrise/sunset _ = sun_rise_set(year, month, day, longitude, latitude, &sunrise, &sunset); // Get civil twilight (dawn/dusk) _ = civil_twilight(year, month, day, longitude, latitude, &dawn, &dusk); // Calculate solar noon (zenith) const zenith = (sunrise + sunset) / 2.0; return .{ .dawn = Time.init(dawn, time.year, time.month, time.day), .sunrise = Time.init(sunrise, time.year, time.month, time.day), .zenith = Time.init(zenith, time.year, time.month, time.day), .sunset = Time.init(sunset, time.year, time.month, time.day), .dusk = Time.init(dusk, time.year, time.month, time.day), }; } test "astronomical calculations" { // Test for London on 2024-01-01 const timestamp: i64 = 1704067200; const astro = init(51.5074, -0.1278, timestamp); // Winter in London: sunrise around 8am, sunset around 4pm UTC // dawn: 07:26, sunrise: 08:06, zenith: 12:03, sunset: 16:01, dusk: 16:41 try std.testing.expectEqual(@as(u5, 7), astro.dawn.hour); try std.testing.expectEqual(@as(u6, 26), astro.dawn.minute); try std.testing.expectEqual(@as(u5, 8), astro.sunrise.hour); try std.testing.expectEqual(@as(u6, 6), astro.sunrise.minute); try std.testing.expectEqual(@as(u5, 12), astro.zenith.hour); try std.testing.expectEqual(@as(u6, 3), astro.zenith.minute); try std.testing.expectEqual(@as(u5, 16), astro.sunset.hour); try std.testing.expectEqual(@as(u6, 1), astro.sunset.minute); try std.testing.expectEqual(@as(u5, 16), astro.dusk.hour); try std.testing.expectEqual(@as(u6, 41), astro.dusk.minute); // Sanity checks try std.testing.expect(astro.dawn.hour < astro.sunrise.hour); try std.testing.expect(astro.sunset.hour <= astro.dusk.hour); try std.testing.expect(astro.zenith.hour > astro.sunrise.hour and astro.zenith.hour < astro.sunset.hour); } test "Oregon modern time" { // Test for Oregon 2026-01-06 const timestamp: i64 = 1767722166; const coords: Coordinates = .{ .latitude = 44.052071, .longitude = -123.086754, }; const astro = init(coords.latitude, coords.longitude, timestamp); const sunrise = try astro.sunrise.adjustTimeToLocal(coords); // Sunrise at 7:47, sunset 16:49, zenith 12:18 try std.testing.expectEqualDeep(Time{ .year = 2026, .month = .jan, .day = 6, .hour = 7, .minute = 46, .offset = -480, }, sunrise); // UTC times: dawn: 15:14, sunrise: 15:46, zenith: 20:18, sunset: 24:49 (00:49 next day), dusk: 25:22 (01:22 next day) // Local PST times: dawn: 07:14, sunrise: 07:46, zenith: 12:18, sunset: 16:49, dusk: 17:22 const sunset = try astro.sunset.adjustTimeToLocal(coords); const dusk = try astro.dusk.adjustTimeToLocal(coords); const zenith = try astro.zenith.adjustTimeToLocal(coords); const dawn = try astro.dawn.adjustTimeToLocal(coords); try std.testing.expectEqual(@as(u5, 7), dawn.hour); try std.testing.expectEqual(@as(u6, 14), dawn.minute); try std.testing.expectEqual(@as(u5, 12), zenith.hour); try std.testing.expectEqual(@as(u6, 18), zenith.minute); try std.testing.expectEqual(@as(u5, 16), sunset.hour); try std.testing.expectEqual(@as(u6, 49), sunset.minute); try std.testing.expectEqual(@as(u5, 17), dusk.hour); try std.testing.expectEqual(@as(u6, 22), dusk.minute); // Sanity checks try std.testing.expect(dawn.hour <= sunrise.hour); try std.testing.expect(dawn.minute < sunrise.minute or dawn.hour < sunrise.hour); try std.testing.expect(sunset.hour < dusk.hour or (sunset.hour == dusk.hour and sunset.minute < dusk.minute)); try std.testing.expect(zenith.hour > sunrise.hour and zenith.hour < sunset.hour); } test "format time" { const time1 = Time{ .year = 2026, .month = .jan, .day = 6, .hour = 12, .minute = 30, .offset = 0 }; const time2 = Time{ .year = 2026, .month = .jan, .day = 6, .hour = 8, .minute = 15, .offset = 0 }; var buf1: [5]u8 = undefined; var buf2: [5]u8 = undefined; var writer1 = std.Io.Writer.fixed(&buf1); var writer2 = std.Io.Writer.fixed(&buf2); try time1.format(&writer1); try time2.format(&writer2); try std.testing.expectEqualStrings("12:30", &buf1); try std.testing.expectEqualStrings("08:15", &buf2); }