#![allow(non_upper_case_globals)] #![allow(invalid_value)] #![allow(non_snake_case)] #![allow(dead_code)] // based on https://prospertimes.neocities.org/solarterms.js use num::cast; use std::{ marker::Freeze, mem::{MaybeUninit, transmute}, ops::{Div, Index, IndexMut, Rem}, sync::LazyLock, }; // this is so stupid. i should not have to put pub every single line pub struct Term { pub solar_term: &'static str, pub ang: usize, pub month: usize, pub earliest_day: usize, pub latest_day: usize, } #[cfg_attr(not(target_family = "wasm"), derive(Debug))] pub struct SexagenaryDate { pub year: &'static str, pub month: &'static str, pub day: &'static str, pub term: Option<(&'static str, usize)>, } const UNIT_YR: usize = 1984; // cant use [].map(|(...)| Term {}) on consts // this sucks, i should not have2 write design8ed initializers each&everytime macro_rules! terms { ($({ $s: expr, $a: expr, $m: expr, $e: expr, $l: expr }),* $(,)?) => { [ $(Term { solar_term: $s, ang: $a, month: $m, earliest_day: $e, latest_day: $l, }),* ] }; } pub const TERMS: [Term; 24] = terms![ {"小寒", 285, 1, 3, 7}, {"大寒", 300, 1, 18, 22}, {"立春", 315, 2, 2, 6}, {"雨水", 330, 2, 16, 20}, {"驚蟄", 345, 3, 3, 7}, {"春分", 360, 3, 18, 22}, {"淸明", 15, 4, 2, 6}, {"穀雨", 30, 4, 18, 22}, {"立夏", 45, 5, 3, 7}, {"小滿", 60, 5, 19, 23}, {"芒種", 75, 6, 3, 7}, {"夏至", 90, 6, 19, 23}, {"小暑", 105, 7, 5, 9}, {"大暑", 120, 7, 20, 24}, {"立秋", 135, 8, 5, 9}, {"處暑", 150, 8, 21, 25}, {"白露", 165, 9, 5, 9}, {"秋分", 180, 9, 21, 25}, {"寒露", 195, 10, 6, 10}, {"霜降", 210, 10, 21, 25}, {"立冬", 225, 11, 5, 9}, {"小雪", 240, 11, 20, 24}, {"大雪", 255, 12, 5, 9}, {"冬至", 270, 12, 19, 23}, ]; // julian d8 fn compute_JD(mut y: f64, mut m: f64, d: f64) -> f64 { if m <= 2. { y -= 1.; m += 12.; } (365.25 * (y + 4716.)).floor() + (30.6001 * (m + 1.)).floor() + d - 13. - 1524.5 } macro_rules! JD { ($y: expr, $m: expr, $d: expr) => { compute_JD($y as f64, $m as f64, $d as f64) }; } // dont ask me how this works fn compute_ang(jd: f64) -> f64 { let T = (jd - 2451545.) / 36525.; let Lmean = (280.46646 + (36000.76983 * T) + (0.0003032 * T * T)) % 360.; let M = (357.52911 + (35999.05029 * T) - (0.0001537 * T * T)) % 360.; let C = ((1.914602 - (0.004817 * T) - (0.000014 * T * T)) * (M.to_radians()).sin()) + ((0.019993 - (0.000101 * T)) * (2. * M.to_radians()).sin()) + (0.000289 * (3. * M.to_radians()).sin()); let Ltrue = Lmean + C; Ltrue - 0.00569 - (0.00478 * ((125.04 - 1934.136 * T).to_radians()).sin()) } #[inline(always)] fn bisect( lo: &mut f64, hi: &mut f64, base: T, scale: T, target: f64, y: usize, m: f64, ) where T: Into + Copy, { while lo <= hi { let mid = f64::midpoint(*lo, *hi).floor(); if compute_ang(JD!(y, m, base.into() + mid * scale.into())) < target { *lo = mid + 1.; } else { *hi = mid - 1.; } } } fn compute_solarterm_day(i: usize, y: usize) -> f64 { //double solarterm_day, test_day, test_hour, test_minute, test_long; let mut solarterm_day; let m: f64 = TERMS[i].month as f64; let mut epd: f64 = TERMS[i].earliest_day as f64; // earliest possible day let mut lpd: f64 = TERMS[i].latest_day as f64; // latest possible day let mut eph: f64 = 0.; // earliest possible hour let mut lph: f64 = 23.; // latest possible hour let mut epm: f64 = 0.; // earliest possible minute let mut lpm: f64 = 59.; // latest possible minute let target_long: f64 = TERMS[i].ang as f64; bisect(&mut epd, &mut lpd, 0, 1, target_long, y, m); solarterm_day = lpd; bisect( &mut eph, &mut lph, solarterm_day, 1. / 24., target_long, y, m, ); solarterm_day += lph / 24.; bisect( &mut epm, &mut lpm, solarterm_day, 1. / 1440., target_long, y, m, ); solarterm_day + (lpm + 1.) / 1440. } #[inline(always)] fn align(a: T, to: T, zero: T) -> usize where T: cast::AsPrimitive, { let (a, to, zero) = (a.as_(), to.as_(), zero.as_()); let tmp = a + (to - zero); (tmp - to * isize::from(tmp >= to)) as usize } #[inline(always)] fn mod2ganzhi(g: T, z: T) -> usize where T: cast::AsPrimitive, { let (g, z) = (g.as_(), z.as_()); // chinese remainder theorem // x ≡ g (mod 10) // x ≡ z (mod 12) // 10x + 12y = gcd(10,12) = 2 // x=-1, y=1 // X=(g-z)/gcd(10,12)=(g-z)/2 // 10xX + 12yX = 2X // => -10X + 12X = 2X // => -5(g-z) + 6(g-z) = g-z // x = g-(-5(g-z)) (case 1) = 6(g-z)+z (case 2) // x = g+5(g-z) = 6g-5z // dont ask me how the math works idk align(6 * g - 5 * z, 60, 0) } // need to do this because // 1. unsafecell doesnt implement Sync // 2. mutating a slice directly will put it in .rodata for normal targets // where i test this on struct Cell { val: T, } // SAFETY(lol): wasm isolates are single-core. unsafe impl Sync for Cell {} // technically i dont need this because wasm mutable globals are enabled, // and Freeze is only needed for targets with a separate .rodata // i dont really care either way but this is how UnsafeCell does it // // lang = "unsafe_cell" (apparently an extremely integral part of rust) // does additional stuff but this is sufficient for the compiler to not // put it in .rodata for normal targets impl !Freeze for Cell {} impl + Index> Cell { pub fn get(&self, i: usize) -> &mut T::Output { // SAFETY: see above let a: &mut T = unsafe { transmute(&self.val) }; &mut a[i] } } fn stday(i: usize, y: usize) -> f64 { const YEARS: usize = 200; const ARRLEN: usize = YEARS * TERMS.len(); // rust doesnt have (*a)[n], | @ least ill have2 use a cr8 4 it // also lazy_static doesnt work with mutables (im not using mutex) static STDAYS: Cell<[f64; ARRLEN]> = Cell { val: [0.; ARRLEN] }; let idx = y - UNIT_YR; assert!((0..YEARS).contains(&idx)); let ret = STDAYS.get(idx * TERMS.len() + i); (if int(*ret) != 0 { *ret } else { let d = compute_solarterm_day(i, y); *ret = d; d }) .floor() } #[must_use] pub fn ganzhi(i: usize) -> &'static str { static GANZHIS: LazyLock> = LazyLock::new(|| { const gan: [char; 10] = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸']; const zhi: [char; 12] = [ '子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥', ]; (0..60).fold(Vec::with_capacity(60), |mut v, i| { v.push([gan[i % 10], zhi[i % 12]].iter().collect()); v }) }); &GANZHIS[i] } #[inline(always)] const fn int(x: f64) -> usize { x as usize // x.as_int_unchecked() // wont work on wasm } #[must_use] pub fn solar(y: usize, m: usize, d: usize) -> SexagenaryDate { static JIAZI: LazyLock = LazyLock::new(|| int(JD!(UNIT_YR, 1, 31))); let jdf = JD!(y, m, d); let jd = int(jdf); let a = compute_ang(jdf); let ygz = (y - UNIT_YR - usize::from((m <= 2 && a < 316.) && (m < 2 || d < int(stday(2, y))))) % 60; let rem = a % 15.; let div = int(a.div(15.).floor()); let mut dz = align(div.div_ceil(2), 12, 9); let mut termb = rem > 14.; // SAFETY: guarded by termb let mut term: usize = unsafe { MaybeUninit::uninit().assume_init() }; if termb { term = align(div, 24, 18); let termday = stday(term, y); termb = d == int(termday); if (div & 1) == 0 { dz += usize::from(termb); dz = align(dz, 12, 12); } } let mut tmp = ygz.rem(5); tmp = tmp * 2 + 2; tmp -= 10 * usize::from(tmp == 10); let mgz = mod2ganzhi(tmp + align(dz, 12, 2).rem(10), dz); let dgz = (jd - *JIAZI).rem(60); SexagenaryDate { year: ganzhi(ygz), month: ganzhi(mgz), day: ganzhi(dgz), term: termb.then(|| (TERMS[term].solar_term, term)), } } // https://en.wikipedia.org/wiki/Determination_of_the_day_of_the_week#Disparate_variation #[must_use] pub fn dow(y: usize, mut m: usize, d: usize) -> (&'static str, &'static str) { let Y = y - usize::from(m <= 2); let y2 = Y % 100; let c = Y / 100; m += 9; m -= 12 * usize::from(m >= 12); let a = (d + (26 * (m + 1) - 2) / 10 + y2 + y2 / 4 + c / 4 - 2 * c) % 7; [ ("日", "Sun"), ("月", "Mon"), ("火", "Tue"), ("水", "Wed"), ("木", "Thu"), ("金", "Fri"), ("土", "Sat"), ][a] } #[cfg(not(target_family = "wasm"))] fn main() { (1..=12).for_each(|m| { (1..28).for_each(|d| { println!("{:?}", solar(2026, m, d)); }); }); }