Places

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

#dates#geo#outdoors#photography

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 error

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.

sun_position(lat: float, lon: float, at: int) → anyor an error

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.

Get it

$ quillo install sun_times               # sun_times.qwer into this folder$ quillo install sun_times --example     # and its example, sun_times.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/sun_times.qwer$ curl -O https://quillolang.org/plugins/files/sun_times.qwe

Then, in your program, all of it or only what you call:

main.qwe
use sun_times.qweruse sun_times, sun_position from sun_times      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]

Example

sun_times.qwe
// 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

sun_times.qwer
//! 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 }))}
Thread
77Useful? Like it, or add to its thread below.

Thread 0 posts

On Sun times: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.