1mod utils;
2
3use wasm_bindgen::prelude::*;
4
5use std::{convert::TryFrom, time::Duration};
6
7use chrono::{DateTime, Datelike, Days, TimeDelta, TimeZone, Timelike};
8use num_ordinal::Ordinal;
9use sunrise::sunrise_sunset;
10
11include!(concat!(env!("OUT_DIR"), "/year_owner.rs"));
12
13#[wasm_bindgen]
14pub struct RomanTime {
15 day: u32,
16 month: u32,
17 year: i32,
18 hour: i32,
19 hour_progress: f64,
20 daylight_length: chrono::Duration,
21}
22
23#[cfg(target_arch = "wasm32")]
24#[wasm_bindgen]
25impl RomanTime {
26 #[wasm_bindgen(constructor)]
27 pub fn new_js(time: js_sys::Date, tz: String, lat: f64, lng: f64) -> Self {
28 use chrono::prelude::*;
29
30 use chrono_tz::Tz;
31
32 let tz: Tz = tz.parse().unwrap();
33
34 let time = NaiveDate::from_ymd_opt(
35 time.get_full_year() as i32,
36 time.get_month() + 1,
37 time.get_date(),
38 )
39 .unwrap()
40 .and_hms_opt(time.get_hours(), time.get_minutes(), time.get_seconds())
41 .unwrap();
42
43 let time = tz.from_local_datetime(&time).unwrap();
44 RomanTime::new(time, lat, lng)
45 }
46}
47
48impl RomanTime {
49 pub fn new<Tz: TimeZone>(time: DateTime<Tz>, lat: f64, lng: f64) -> Self {
50 let (sunrise, sunset) = sunrise_sunset(lat, lng, time.year(), time.month(), time.day());
51 let timezone = time.timezone();
52 let time = time.naive_local();
53 let midnight_today = time
54 .with_hour(0)
55 .unwrap()
56 .with_minute(0)
57 .unwrap()
58 .with_second(0)
59 .unwrap();
60 let sunrise = chrono::Utc
61 .timestamp_opt(sunrise, 0)
62 .unwrap()
63 .with_timezone(&timezone)
64 .naive_local();
65 let sunset = chrono::Utc
66 .timestamp_opt(sunset, 0)
67 .unwrap()
68 .with_timezone(&timezone)
69 .naive_local();
70
71 let (hour, hour_progress) = if time < sunrise {
72 let time_since_midnight = time - midnight_today;
74 let morning_night_length = sunrise - midnight_today;
75 let (hour, fract) = hour_breakdown(time_since_midnight, morning_night_length, 6);
76 (hour + 6 + 12, fract)
77 } else if time > sunset {
78 let midnight_tomorrow = midnight_today + Duration::from_secs(24 * 60 * 60);
80 let time_since_sunset = time - sunset;
81 let evening_night_length = midnight_tomorrow - sunset;
82 let (hour, fract) = hour_breakdown(time_since_sunset, evening_night_length, 6);
83 (hour + 12, fract)
84 } else {
85 let time_since_sunrise = time - sunrise;
87 let daylight_length = sunset - sunrise;
88 let (hour, fract) = hour_breakdown(time_since_sunrise, daylight_length, 12);
89 (hour, fract)
90 };
91
92 let roman_date = if time < sunrise {
93 time.checked_sub_days(Days::new(1)).unwrap()
94 } else {
95 time
96 };
97
98 return RomanTime {
101 day: roman_date.day(),
102 month: roman_date.month(),
103 year: roman_date.year(),
104 hour,
105 hour_progress,
106 daylight_length: chrono::Duration::seconds((sunset - sunrise).num_seconds()),
107 };
108 }
109
110 pub fn daylight_length(&self) -> chrono::Duration {
111 self.daylight_length
112 }
113
114 pub fn night_length(&self) -> chrono::Duration {
115 chrono::Duration::seconds(24 * 60 * 60) - self.daylight_length
116 }
117}
118
119const FULL_MONTHS: [u32; 7] = [1, 3, 5, 7, 8, 10, 12];
120
121#[wasm_bindgen]
122impl RomanTime {
123 pub fn year(&self) -> i32 {
124 self.year
125 }
126
127 pub fn month(&self) -> u32 {
128 self.month
129 }
130
131 pub fn day(&self) -> u32 {
132 self.day
133 }
134
135 pub fn hour(&self) -> i32 {
136 self.hour
137 }
138
139 pub fn hour_progress(&self) -> f64 {
140 self.hour_progress
141 }
142
143 #[cfg(target_arch = "wasm32")]
144 pub fn daylight_length_seconds(&self) -> i64 {
145 self.daylight_length().num_seconds()
146 }
147
148 #[cfg(target_arch = "wasm32")]
149 pub fn night_length_seconds(&self) -> i64 {
150 self.night_length().num_seconds()
151 }
152
153 pub fn year_string(&self, country_iso_3166: &str) -> String {
154 let owners: &[YearOwner] = match get_owners_for_country(country_iso_3166) {
155 Some(owners) => owners,
156 None => return self.year().to_string(),
157 };
158
159 for owner in owners.iter() {
160 for (i, years) in owner.years.iter().enumerate() {
161 if self.year >= years.0 && self.year <= years.1 {
162 let mut year_count = 0;
163 for j in 0..i {
164 year_count += (owner.years[j].1 - owner.years[j].0) + 1;
165 }
166
167 year_count += self.year - years.0;
168
169 year_count += 1;
170
171 return format!(
172 "{} year of {}",
173 num_ordinal::Osize::from1(year_count as usize),
174 owner.owner
175 );
176 }
177 }
178 }
179
180 return self.year().to_string();
181 }
182
183 pub fn date_string(&self) -> String {
184 let is_full_month = FULL_MONTHS.iter().any(|&x| x == self.month);
185
186 let month = chrono::Month::try_from((self.month) as u8).unwrap();
187 let month_string = month.name().to_string();
188
189 if self.day == 1 {
190 return "Kalends of ".to_string() + &month_string;
191 }
192
193 let nones_date = if is_full_month { 7 } else { 5 };
194 if self.day <= nones_date {
195 let remaining = nones_date - self.day;
196 if remaining == 0 {
197 return format!("Nones of {}", month_string);
198 } else if remaining == 1 {
199 return format!("day before the Nones of {}", month_string);
200 }
201 return format!(
202 "{} day before the Nones of {}",
203 num_ordinal::Osize::from1(remaining as usize + 1),
204 month_string
205 );
206 }
207
208 let ides_date = if is_full_month { 15 } else { 13 };
209 if self.day <= ides_date {
210 let remaining = ides_date - self.day;
211 if remaining == 0 {
212 return format!("Ides of {}", month_string);
213 } else if remaining == 1 {
214 return format!("day before the Ides of {}", month_string);
215 }
216 return format!(
217 "{} day before the Ides of {}",
218 num_ordinal::Osize::from1(remaining as usize + 1),
219 month_string
220 );
221 }
222
223 let next_month = (self.month + 1) % 12;
225 let next_month_name = chrono::Month::try_from(next_month as u8).unwrap().name();
226 let leap_year = self.year % 4 == 0 && (self.year % 100 != 0 || self.year % 400 == 0);
227 let days_in_month = match self.month {
228 2 => {
229 if leap_year {
230 29
231 } else {
232 28
233 }
234 }
235 4 | 6 | 9 | 11 => 30,
236 _ => 31,
237 };
238
239 let remaining = days_in_month - self.day;
240 if remaining == 0 {
241 return format!("day before the Kalends of {}", next_month_name);
242 }
243
244 format!(
245 "{} day before the Kalends of {}",
246 num_ordinal::Osize::from1(remaining as usize + 2),
247 next_month_name
248 )
249 }
250
251 pub fn hour_string(&self) -> String {
252 let hour = self.hour() + 1;
253
254 let progress_part = if self.hour_progress <= 0.25 {
255 "less than a quarter"
256 } else if self.hour_progress <= 0.5 {
257 "less than half"
258 } else if self.hour_progress <= 0.75 {
259 "less than three quarters"
260 } else {
261 "more than three quarters"
262 };
263
264 let night_time = hour >= 13;
265 let hour = if hour > 12 { hour - 12 } else { hour };
266
267 format!(
268 "{} of the {} {} hour",
269 progress_part,
270 num_ordinal::Osize::from1(hour as usize),
271 if night_time { "night" } else { "daylight" }
272 )
273 }
274
275 pub fn to_string(&self, country_iso_3166: &str) -> String {
276 format!(
277 "{} of {} {}",
278 self.hour_string(),
279 self.date_string(),
280 self.year_string(country_iso_3166),
281 )
282 }
283}
284
285fn hour_breakdown(time_since: TimeDelta, total_length: TimeDelta, hour_amount: u8) -> (i32, f64) {
286 let hour_length = total_length.num_seconds() as f64 / hour_amount as f64;
287 let hour = time_since.num_seconds() as f64 / hour_length;
288 (hour.floor() as i32, hour.fract())
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294 use itertools::Itertools;
295
296 const BROKEN_HILL_LAT: f64 = -31.9596256;
297 const BROKEN_HILL_LNG: f64 = 141.4575006;
298
299 #[test]
300 fn broken_hill_before_sunrise() {
301 let time = chrono_tz::Australia::Broken_Hill
302 .with_ymd_and_hms(2025, 01, 27, 6, 20, 0)
303 .unwrap();
304 let roman_time = RomanTime::new(time, BROKEN_HILL_LAT, BROKEN_HILL_LNG);
305 assert_eq!(roman_time.day(), 26);
306 assert_eq!(roman_time.hour(), 23);
307 assert!(roman_time.hour_progress() > 0.9);
308 }
309
310 #[test]
311 fn broken_hill_just_after_sunrise() {
312 let time = chrono_tz::Australia::Broken_Hill
313 .with_ymd_and_hms(2025, 01, 27, 6, 25, 0)
314 .unwrap();
315 let roman_time = RomanTime::new(time, BROKEN_HILL_LAT, BROKEN_HILL_LNG);
316 assert_eq!(roman_time.day(), 27);
317 assert_eq!(roman_time.hour(), 0);
318 assert!(roman_time.hour_progress() < 0.1);
319 }
320
321 #[test]
322 fn broken_hill_just_after_sunset() {
323 let time = chrono_tz::Australia::Broken_Hill
324 .with_ymd_and_hms(2025, 01, 27, 20, 10, 0)
325 .unwrap();
326 let roman_time = RomanTime::new(time, BROKEN_HILL_LAT, BROKEN_HILL_LNG);
327 assert_eq!(roman_time.day(), 27);
328 assert_eq!(roman_time.hour(), 12);
329 assert!(roman_time.hour_progress() < 0.1);
330 }
331
332 #[test]
333 fn broken_hill_solar_noon() {
334 let time = chrono_tz::Australia::Broken_Hill
335 .with_ymd_and_hms(2025, 01, 27, 13, 16, 0)
336 .unwrap();
337 let roman_time = RomanTime::new(time, BROKEN_HILL_LAT, BROKEN_HILL_LNG);
338 assert_eq!(roman_time.hour(), 5);
339 assert!(roman_time.hour_progress() > 0.9);
340 }
341
342 #[test]
343 fn before_sunrise_first_of_month() {
344 let time = chrono_tz::Australia::Broken_Hill
345 .with_ymd_and_hms(2025, 01, 01, 1, 0, 0)
346 .unwrap();
347 let roman_time = RomanTime::new(time, BROKEN_HILL_LAT, BROKEN_HILL_LNG);
348 assert_eq!(roman_time.day(), 31);
349 assert_eq!(roman_time.month(), 12);
350 assert_eq!(roman_time.year(), 2024);
351 }
352
353 #[test]
354 fn year_string() {
355 let time = chrono_tz::Australia::Broken_Hill
356 .with_ymd_and_hms(1996, 01, 27, 12, 45, 0)
357 .unwrap();
358 let roman_time = RomanTime::new(time, BROKEN_HILL_LAT, BROKEN_HILL_LNG);
359 assert_eq!(roman_time.year_string("AU"), "5th year of Paul Keating");
360
361 let time = chrono_tz::Australia::Broken_Hill
362 .with_ymd_and_hms(1941, 02, 01, 1, 0, 0)
363 .unwrap();
364 let roman_time = RomanTime::new(time, BROKEN_HILL_LAT, BROKEN_HILL_LNG);
365 assert_eq!(
366 roman_time.year_string("AU"),
367 "second year of Robert Menzies"
368 );
369
370 let time = chrono_tz::Australia::Broken_Hill
371 .with_ymd_and_hms(1951, 02, 01, 1, 0, 0)
372 .unwrap();
373 let roman_time = RomanTime::new(time, BROKEN_HILL_LAT, BROKEN_HILL_LNG);
374 assert_eq!(roman_time.year_string("AU"), "4th year of Robert Menzies");
375 assert_eq!(roman_time.year_string("US"), "6th year of Harry S Truman");
376 }
377
378 #[test]
379 fn date_string() {
380 let time = chrono_tz::Australia::Broken_Hill
381 .with_ymd_and_hms(1996, 02, 1, 3, 45, 0)
382 .unwrap();
383 let roman_time = RomanTime::new(time, BROKEN_HILL_LAT, BROKEN_HILL_LNG);
384 assert_eq!(
385 roman_time.date_string(),
386 "day before the Kalends of February"
387 );
388
389 let time = chrono_tz::Australia::Broken_Hill
390 .with_ymd_and_hms(1996, 01, 27, 12, 45, 0)
391 .unwrap();
392 let roman_time = RomanTime::new(time, BROKEN_HILL_LAT, BROKEN_HILL_LNG);
393 assert_eq!(
394 roman_time.date_string(),
395 "6th day before the Kalends of February"
396 );
397
398 let time = chrono_tz::Australia::Broken_Hill
399 .with_ymd_and_hms(1996, 03, 3, 12, 45, 0)
400 .unwrap();
401 let roman_time = RomanTime::new(time, BROKEN_HILL_LAT, BROKEN_HILL_LNG);
402 assert_eq!(
403 roman_time.date_string(),
404 "5th day before the Nones of March"
405 );
406
407 let time = chrono_tz::Australia::Broken_Hill
408 .with_ymd_and_hms(1996, 03, 7, 7, 45, 0)
409 .unwrap();
410 let roman_time = RomanTime::new(time, BROKEN_HILL_LAT, BROKEN_HILL_LNG);
411 assert_eq!(roman_time.date_string(), "Nones of March");
412
413 let time = chrono_tz::Australia::Broken_Hill
414 .with_ymd_and_hms(1996, 03, 15, 7, 45, 0)
415 .unwrap();
416 let roman_time = RomanTime::new(time, BROKEN_HILL_LAT, BROKEN_HILL_LNG);
417 assert_eq!(roman_time.date_string(), "Ides of March");
418
419 let time = chrono_tz::Australia::Broken_Hill
420 .with_ymd_and_hms(1996, 03, 14, 7, 45, 0)
421 .unwrap();
422 let roman_time = RomanTime::new(time, BROKEN_HILL_LAT, BROKEN_HILL_LNG);
423 assert_eq!(roman_time.date_string(), "day before the Ides of March");
424
425 let time = chrono_tz::Australia::Broken_Hill
426 .with_ymd_and_hms(1996, 03, 13, 7, 45, 0)
427 .unwrap();
428 let roman_time = RomanTime::new(time, BROKEN_HILL_LAT, BROKEN_HILL_LNG);
429 assert_eq!(
430 roman_time.date_string(),
431 "third day before the Ides of March"
432 );
433 }
434
435 #[test]
436 fn hour_string() {
437 let time = chrono_tz::Australia::Broken_Hill
438 .with_ymd_and_hms(2025, 01, 28, 22, 48, 0)
439 .unwrap();
440 let roman_time = RomanTime::new(time, BROKEN_HILL_LAT, BROKEN_HILL_LNG);
441 assert_eq!(
442 roman_time.hour_string(),
443 "less than a quarter of the 5th night hour"
444 );
445
446 let time = chrono_tz::Australia::Broken_Hill
447 .with_ymd_and_hms(1996, 01, 27, 12, 45, 0)
448 .unwrap();
449 let roman_time = RomanTime::new(time, BROKEN_HILL_LAT, BROKEN_HILL_LNG);
450 assert_eq!(
451 roman_time.hour_string(),
452 "less than three quarters of the 6th daylight hour"
453 );
454
455 let time = chrono_tz::Australia::Broken_Hill
456 .with_ymd_and_hms(2025, 01, 28, 6, 48, 0)
457 .unwrap();
458 let roman_time = RomanTime::new(time, BROKEN_HILL_LAT, BROKEN_HILL_LNG);
459 assert_eq!(
460 roman_time.hour_string(),
461 "less than half of the first daylight hour"
462 );
463 }
464
465 #[test]
466 fn to_string() {
467 for (hour, minute, second) in (0..24)
468 .cartesian_product(0..60)
469 .cartesian_product(0..60)
470 .map(|((h, m), s)| (h, m, s))
471 {
472 let time = chrono_tz::Australia::Broken_Hill
473 .with_ymd_and_hms(2025, 01, 28, hour, minute, second)
474 .unwrap();
475 let roman_time = RomanTime::new(time, BROKEN_HILL_LAT, BROKEN_HILL_LNG);
476 assert!(roman_time.to_string("AU").len() > 0);
477 }
478 }
479
480 #[test]
481 fn daylight_length() {
482 let time = chrono_tz::Australia::Broken_Hill
483 .with_ymd_and_hms(2025, 01, 28, 6, 48, 0)
484 .unwrap();
485 let roman_time = RomanTime::new(time, BROKEN_HILL_LAT, BROKEN_HILL_LNG);
486 assert_eq!(roman_time.daylight_length().num_seconds() / 12 / 60, 68);
487 }
488}