Sun times
Sunrise, sunset, day length, twilight and golden hour for a place and date, and where the sun is at a moment; midnight sun and polar night told.
Version 1.0.0 · 2 functions · 0 crates
Functions
What use sun_times.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use sun_times, sun_position from sun_times (Quillo 0.6).
sun_times(lat: float, lon: float, date: str, utc_offset_minutes: int) → anyor an errorThe sun's day at a place on date (2026-06-21), times given at utc_offset_minutes (120 for UTC+2; the zone's offset on that date): { sunrise, sunset, solar_noon, day_length, day_minutes, civil_dawn, civil_dusk, golden_hour, polar } (golden_hour: when the evening's begins, the sun 6° up). Under the midnight sun polar is "day" (day_minutes 1440), in polar night "night" (0), and the times it lacks are null. Accurate to about a minute.
sun_position(lat: float, lon: float, at: int) → anyor an errorWhere the sun is at a place at the Unix time at (seconds; 0 for now): { elevation, azimuth, up }, degrees; azimuth from north, clockwise; up when its top is above the horizon.
Get it
$ quillo install sun_times # sun_times.qwer into this folder$ quillo install sun_times --example # and its example, sun_times.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/sun_times.qwer$ curl -O https://quillolang.org/plugins/files/sun_times.qweThen, in your program, all of it or only what you call:
use sun_times.qweruse sun_times, sun_position from sun_times // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
Example
// A campsite's booking page: today's sunrise and sunset, the evening's golden hour for photos,// and the far north, where in June the sun does not set.//// quillo run sun_times.qwe then open http://localhost:8080/sun?date=2026-06-21use "sun_times.qwer"route GET /sun ? { date: str = "2026-06-21", lat: float = 52.37, lon: float = 4.89, offset: int = 120 } { let s = sun_times(lat, lon, date, offset)? { sunrise: slice(s.sunrise, 11, 16), sunset: slice(s.sunset, 11, 16), golden_hour: slice(s.golden_hour ?? "", 11, 16), day: s.day_length }}route GET /north = { tromso_june: sun_times(69.65, 18.96, "2026-06-21", 120)?.polar, tromso_december: sun_times(69.65, 18.96, "2026-12-21", 60)?.day_minutes }// 21 June 2026, 12:00 UTC in Amsterdam: the sun is high in the south.route GET /now = sun_position(52.37, 4.89, 1782043200)?$ quillo run sun_times.qwe$ curl 'localhost:8080/sun?date=2026-06-21'$ curl 'localhost:8080/north'$ curl 'localhost:8080/now'Source
//! Sunrise, sunset, day length and twilight for a place and a date, and where the sun is at a moment: for outdoor bookings, photography, farming, "open until sunset" and dark themes after dusk. Midnight sun and polar night are told, not guessed.//! version: 1.0.0//!//! [dependencies]use serde_json::{json, Value};const J2000: f64 = 2451545.0;fn days_from_civil(y: i64, m: i64, d: i64) -> i64 { let (y, m) = if m <= 2 { (y - 1, m + 9) } else { (y, m - 3) }; let era = y.div_euclid(400); let yoe = y - era * 400; let doy = (153 * m + 2) / 5 + d - 1; era * 146097 + yoe * 365 + yoe / 4 - yoe / 100 + doy - 719468}fn civil(z: i64) -> (i64, i64, i64) { let z = z + 719468; let era = z.div_euclid(146097); let doe = z - era * 146097; let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let d = doy - (153 * mp + 2) / 5 + 1; let m = if mp < 10 { mp + 3 } else { mp - 9 }; (yoe + era * 400 + if m <= 2 { 1 } else { 0 }, m, d)}fn parse_date(s: &str) -> Result<i64, String> { let p: Vec<i64> = s.trim().split('-').map(|x| x.parse::<i64>()).collect::<Result<_, _>>().map_err(|_| format!("`{}` is not a date like 2026-06-21", s))?; if p.len() != 3 || !(1..=12).contains(&p[1]) || !(1..=31).contains(&p[2]) { return Err(format!("`{}` is not a date like 2026-06-21", s)); } Ok(days_from_civil(p[0], p[1], p[2]))}/// The sun's declination (radians), the Julian date of solar noon, for a day and a longitude (Wikipedia's sunrise equation).fn solar(day: i64, lon: f64) -> (f64, f64) { let n = (day as f64 + 2440587.5 + 0.5) - J2000 + 0.0008; let n = n.round(); let jstar = n - lon / 360.0; let m = (357.5291 + 0.98560028 * jstar).rem_euclid(360.0).to_radians(); let c = 1.9148 * m.sin() + 0.02 * (2.0 * m).sin() + 0.0003 * (3.0 * m).sin(); let lambda = (m.to_degrees() + c + 180.0 + 102.9372).rem_euclid(360.0).to_radians(); let transit = J2000 + jstar + 0.0053 * m.sin() - 0.0069 * (2.0 * lambda).sin(); let decl = (lambda.sin() * 23.4397f64.to_radians().sin()).asin(); (decl, transit)}fn unix(j: f64) -> i64 { ((j - 2440587.5) * 86400.0).round() as i64}/// A Unix time as local text with the offset: `2026-06-21T05:18:00+02:00`.fn local(t: i64, offset_min: i64) -> String { let l = t + offset_min * 60; let (y, m, d) = civil(l.div_euclid(86400)); let r = l.rem_euclid(86400); let (sign, o) = if offset_min < 0 { ('-', -offset_min) } else { ('+', offset_min) }; format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}{}{:02}:{:02}", y, m, d, r / 3600, r % 3600 / 60, r % 60, sign, o / 60, o % 60)}/// When the sun's centre crosses `angle` degrees (negative: below the horizon) on that day: (rise, set) in Unix seconds, or why not.fn crossing(lat: f64, day: i64, lon: f64, angle: f64) -> Result<(i64, i64), &'static str> { let (decl, transit) = solar(day, lon); let phi = lat.to_radians(); let cos_w = (angle.to_radians().sin() - phi.sin() * decl.sin()) / (phi.cos() * decl.cos()); if cos_w < -1.0 { return Err("day"); } if cos_w > 1.0 { return Err("night"); } let w = cos_w.acos().to_degrees() / 360.0; Ok((unix(transit - w), unix(transit + w)))}fn place(lat: f64, lon: f64) -> Result<(), String> { if !(-90.0..=90.0).contains(&lat) || !(-180.0..=180.0).contains(&lon) { return Err(format!("{}, {} is not a place", lat, lon)); } Ok(())}/// The sun's day at a place on `date` (`2026-06-21`), times given at `utc_offset_minutes` (120 for UTC+2; the zone's offset on that date):/// `{ sunrise, sunset, solar_noon, day_length, day_minutes, civil_dawn, civil_dusk, golden_hour, polar }` (`golden_hour`: when the evening's begins, the sun 6° up). Under the midnight sun `polar` is "day" (day_minutes 1440),/// in polar night "night" (0), and the times it lacks are null. Accurate to about a minute.pub fn sun_times(lat: f64, lon: f64, date: &str, utc_offset_minutes: i64) -> Result<Value, String> { place(lat, lon)?; let off = utc_offset_minutes.clamp(-14 * 60, 14 * 60); // The local date's noon decides which solar day is meant. let day = parse_date(date)?; let day = (day * 86400 + 43200 - off * 60).div_euclid(86400); let (_, transit) = solar(day, lon); let noon = unix(transit); let sun = crossing(lat, day, lon, -0.833); let civil_t = crossing(lat, day, lon, -6.0); let golden = crossing(lat, day, lon, 6.0); let t = |x: Option<i64>| x.map(|x| json!(local(x, off))).unwrap_or(Value::Null); let minutes = match sun { Ok((r, s)) => (s - r) / 60, Err("day") => 1440, Err(_) => 0, }; Ok(json!({ "sunrise": t(sun.ok().map(|x| x.0)), "sunset": t(sun.ok().map(|x| x.1)), "solar_noon": local(noon, off), "day_length": format!("{}:{:02}", minutes / 60, minutes % 60), "day_minutes": minutes, "civil_dawn": t(civil_t.ok().map(|x| x.0)), "civil_dusk": t(civil_t.ok().map(|x| x.1)), "golden_hour": t(golden.ok().map(|x| x.1)), "polar": sun.err(), }))}/// Where the sun is at a place at the Unix time `at` (seconds; 0 for now): `{ elevation, azimuth, up }`, degrees; azimuth from north, clockwise; `up` when its top is above the horizon.pub fn sun_position(lat: f64, lon: f64, at: i64) -> Result<Value, String> { place(lat, lon)?; let t = if at == 0 { std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs() as i64).unwrap_or(0) } else { at }; let jd = t as f64 / 86400.0 + 2440587.5; let n = jd - J2000; let m = (357.5291 + 0.98560028 * n).rem_euclid(360.0).to_radians(); let c = 1.9148 * m.sin() + 0.02 * (2.0 * m).sin() + 0.0003 * (3.0 * m).sin(); let lambda = (m.to_degrees() + c + 180.0 + 102.9372).rem_euclid(360.0).to_radians(); let eps = 23.4397f64.to_radians(); let decl = (lambda.sin() * eps.sin()).asin(); let ra = (lambda.sin() * eps.cos()).atan2(lambda.cos()); let gmst = (280.46061837 + 360.98564736629 * n).rem_euclid(360.0).to_radians(); let h = gmst + lon.to_radians() - ra; let phi = lat.to_radians(); let elev = (phi.sin() * decl.sin() + phi.cos() * decl.cos() * h.cos()).asin(); let az = (-h.sin()).atan2(phi.cos() * decl.tan() - phi.sin() * h.cos()); let r = |x: f64| (x * 100.0).round() / 100.0; Ok(json!({ "elevation": r(elev.to_degrees()), "azimuth": r(az.to_degrees().rem_euclid(360.0)), "up": elev.to_degrees() > -0.833 }))}Related
timezone_at.qwer · 1.0.0The IANA time zone at a latitude and longitude, offline, and the local time and UTC offset there, summer time counted.7 views · 8 downloadsCoordinates coordinates.qwer · 1.0.0Coordinates as people write them, such as 52°22'N 4°53'E or N 52° 22.2', read as one, written as degrees-minutes-seconds, and converted to and from UTM.7 views · 8 downloadsH3 hexagons h3.qwer · 1.0.0H3 hexagon cells: the cell of a point at a resolution, its neighbours within k rings, its outline for a map, and distances between cells.7 views · 8 downloadsKML and KMZ kml.qwer · 1.0.0KML and KMZ files from Google Earth and My Maps read to GeoJSON, placemarks with their names, descriptions, folders and extended data, and GeoJSON written back as KML or KMZ.7 views · 8 downloadsMaidenhead locators maidenhead.qwer · 1.0.0Maidenhead grid locators as radio amateurs use them (FN31pr, JN58td): from a latitude and longitude at 2 to 10 characters, back to the square's centre and corners, and the distance and bearing between two.7 views · 8 downloadsMap clusters map_clusters.qwer · 1.0.0Map markers clustered by zoom as Supercluster does: thousands of points loaded once, then for each view and zoom the clusters with their counts and the single points, a cluster's points, and the zoom where it opens.7 views · 7 downloadsThread 0 posts
On Sun times: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.