From 4571e203180afbf0b9ad412c08109bba013b08a8 Mon Sep 17 00:00:00 2001 From: u <@> Date: Tue, 10 Mar 2026 08:23:57 +0200 Subject: a --- src/lib.rs | 211 +++++++++++++++++++++++++++++++++++++++ src/solar.rs | 314 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 525 insertions(+) create mode 100644 src/lib.rs create mode 100644 src/solar.rs (limited to 'src') diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..6401829 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,211 @@ +use maud::{html, Markup, PreEscaped, DOCTYPE}; +use regex::Regex; +use worker::*; + +pub mod solar; +use crate::solar::{dow, ganzhi, solar, SexagenaryDate, Term, TERMS}; + +#[event(fetch)] +async fn fetch(req: Request, env: Env, _ctx: Context) -> Result { + let r = Router::new(); + let re = |r: Request, _| Response::redirect(r.url()?.join("shitpit")?); + Router::new() + .get("/journal", re) + .get("/diary", re) + .get("/index.html", |req, _| { + Response::redirect(req.url()?.join("/")?) + }) + .get_async("/shitpit", |req, ctx| async move { + let u = req.url()?; + let a = ctx.env.assets("ASSETS")?; + let mut a = a.fetch(u.join("/_/diary")?, None).await?; + let a = a.text().await?; + let re = Regex::new( + r"(0[1-9]|[1-9]|1[0-2])\/(0[1-9]|[1-9]|1\d|2\d|3[01])\/(\d{2})$", + ).unwrap(); + let mut a: Vec = + a.split('\n').map(|s| s.trim().to_string()).collect(); + bone("shitpit", "look in the sky! it's a bird! it's a plane! no it's superego!", html! { + pre { + @for x in a.into_iter().map(|l| { + if let Some(c) = re.captures(&l) { + let (m, d, y) = (c.get(1), c.get(2), c.get(3)); + let f = |x: Option| x.unwrap().as_str().parse().unwrap(); + let (y, m, d): (usize, usize, usize) = (f(y) + 2000, f(m), f(d)); + let s = solar(y, m, d); + let (dowc, dowl) = dow(y, m, d); + let (y, m, d, t) = (s.year, s.month, s.day, s.term); + let href = l.replace("/", "_"); + html! { + (PreEscaped("")) + a name=(href) { } + a class="sec" href=(format!("#{href}")) { + (format!("{l} {dowl}. {y}年 {m}月 {d}日 ({dowc})")) + @if let Some((t, _)) = t { + (t) + } + } + (PreEscaped("
"))
+                            }
+                        } else {
+                            html! { (PreEscaped(l)) "\n" }
+                        }
+                    }) {
+                        (x)
+                    }
+                }
+            })
+        })
+        .get("/", |_, _| {
+            bone("home", "ataxia.moe: like i heard a transmission from Apollo 13", html! {
+                p style="padding: 1vw" {
+                    "No. Stop." br; br; "Check out "
+                    a href="/shitpit" { "the shitpit" }
+                    " in the meantime."
+                }
+            })
+        })
+        .get("/about", |_, _| {
+            bone("about", "And they completely goddamn disrespected me! Little idiots! Idiots!", html! {
+                p style="padding: 1vw" {
+                    "Ataxia is a nervous system dysfunction consisting of poor coördination of muscle movements such as gait abnormalities, slurring of speech, and eye movement issues."
+                    br; br;
+                    "Hosted on "
+                    a href="https://developers.cloudflare.com/workers/" { "Cloudflare Workers" }
+                    ". Source available once I sort the mess out."
+                }
+            })
+        })
+        .or_else_any_method("/*catchall", |_, _| {
+            Ok(bone("", "404 Not Found", html! {
+                p style="padding: 1vw" { "what?" }
+            })?.with_status(404))
+        })
+        .run(req, env)
+        .await
+}
+
+fn header() -> Markup {
+    let css = r#"/* i dont know css im so fucking sorry ill fix it l8r */
+pre {
+  white-space: pre-wrap;
+  white-space: -moz-pre-wrap;
+  white-space: -pre-wrap;
+  white-space: -o-pre-wrap;
+  word-wrap: break-word;
+}
+.remark {
+  box-sizing: content-box;
+  border-top: 0px solid #000;
+  border-left: 0px solid #000;
+  border-right: 0px solid #000;
+  border-bottom: 1px solid #000;
+  padding: 1vw;
+  padding-top: 0;
+}
+.nav {
+  border-bottom: 1px solid #000;
+  padding: 1vw;
+  padding-top: 0;
+}
+main {
+  border: 1px #000 solid;
+}
+body {
+  background-color: #eee;
+}
+
+@media only screen and (min-width: 800px) {
+
+  p, a.sec, pre {
+    padding: 0 4vw;
+/*align: center;
+display:flex;
+align-items: center;
+justify-content: center;*/
+  }
+}
+pre {
+  font-size: 12pt;
+}
+a, p { font-size: 14pt; }
+a.splink { color: #005953; font-size: inherit; }
+a.sec, a.menu, a.menuon {
+  color: #005953;
+  font-family: 'Iansui', serif;
+  font-size: 1.5em;
+}
+a.sec:link, a.sec:visited, a.sec:active {
+   text-decoration: none;
+}
+a.sec:hover, a.sec:focus {
+   text-decoration: underline;
+}
+a.menu:link, a.menu:visited, a.menu:active {
+   text-decoration: none;
+}
+a.menu:hover, a.menu:focus {
+   text-decoration: underline;
+}
+@media (prefers-color-scheme: dark) {
+  body {
+    background-color: #222;
+    color: #dcdccc;
+  }
+  .nav, .remark {
+    border-bottom: 1px solid #dcdccc;
+  }
+  main {
+    border: 1px #dcdccc solid;
+  }
+  a.splink, a.sec, a.menu, a.menuon { color: #51bd96; }
+}
+@font-face {
+  font-family: 'Iansui';
+  font-style: normal;
+  font-display: swap;
+  font-weight: 400;
+  src: url(/_/iansui.woff2) format('woff2');
+  unicode-range: U+3000-303F,U+3105-312F,U+4E00-9FFF;
+}
+body {
+  max-width: 960px;
+  margin-left: auto;
+  margin-right: auto;
+}"#;
+    html! {
+        (DOCTYPE)
+        style { (PreEscaped(css)) }
+        (PreEscaped(r#""#))
+    }
+}
+
+fn menu(nav: &str) -> Markup {
+    html! {
+        div class="nav" {
+            @let navs = ["home", "shitpit", "about"];
+            @for (i, &n) in navs.iter().enumerate() {
+                a class=({if n == nav { "menuon" } else { "menu" }}) href={"/" (n)} {
+                    (n)
+                }
+                @if i != navs.len() - 1 {
+                    " / "
+                }
+            }
+        }
+    }
+}
+
+fn bone(wh: &str, rm: &str, b: Markup) -> Result {
+    Response::from_html(
+        (html! {
+            (header())
+            main id="content" {
+                h2 class="remark" { (rm) }
+                (menu(wh))
+                (b)
+            }
+        })
+        .into_string(),
+    )
+}
diff --git a/src/solar.rs b/src/solar.rs
new file mode 100644
index 0000000..af52afc
--- /dev/null
+++ b/src/solar.rs
@@ -0,0 +1,314 @@
+#![allow(non_upper_case_globals)]
+#![allow(invalid_value)]
+#![allow(non_snake_case)]
+#![allow(dead_code)]
+
+// based on https://prospertimes.neocities.org/solarterms.js
+use lazy_static::lazy_static;
+use num::{cast, traits::AsPrimitive};
+use std::num::Wrapping as W;
+use std::{
+    f64::consts::PI,
+    mem::MaybeUninit,
+    ops::{Div, Rem},
+};
+
+// 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},
+];
+
+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 * PI / 180.).sin())
+        + ((0.019993 - (0.000101 * T)) * (2. * M * PI / 180.).sin())
+        + (0.000289 * (3. * M * PI / 180.).sin());
+    let Ltrue = Lmean + C;
+    let Lapp = Ltrue
+        - 0.00569
+        - (0.00478 * ((125.04 - 1934.136 * T) * PI / 180.).sin());
+    Lapp
+}
+
+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 = ((*lo + *hi) / 2.).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 * (tmp >= to) as isize) 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)
+}
+
+fn stday(i: usize, y: usize) -> f64 {
+    const YEARS: usize = 200;
+    // rust doesnt have (*a)[n], | @ least ill have2 use a cr8 4 it
+    // also lazy_static doesnt work with mutables (im not using mutex)
+    assert!(y < UNIT_YR + YEARS && UNIT_YR <= y);
+    static mut STDAYS: Vec = vec![];
+    static mut INIT: bool = false;
+    unsafe {
+        if !INIT {
+            STDAYS = vec![0.; YEARS * TERMS.len()];
+            INIT = true;
+        }
+        let idx = y - UNIT_YR;
+        let ret = &mut STDAYS[idx * TERMS.len() + i];
+        (if int(*ret) != 0 {
+            *ret
+        } else {
+            let d = compute_solarterm_day(i, y);
+            *ret = d;
+            d
+        })
+        .floor()
+    }
+}
+
+lazy_static! {
+    static ref GANZHIS: Vec = {
+        let mut tmp: Vec = vec![];
+        tmp.reserve(60);
+        (0..60).for_each(|i| {
+            // looks so much worse than using format!()
+            // cant be bothered to benchmark
+            let mut s = gan[i % 10].to_string();
+            s.push(zhi[i % 12]);
+            tmp.push(s);
+        });
+        tmp
+    };
+}
+const gan: [char; 10] =
+    ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
+const zhi: [char; 12] = [
+    '子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥',
+];
+pub fn ganzhi(i: usize) -> &'static str {
+    &GANZHIS[i]
+}
+
+#[inline(always)]
+fn int(x: f64) -> usize {
+    x as usize
+}
+
+lazy_static! {
+    static ref JIAZI: usize = int(JD!(UNIT_YR, 1, 31));
+}
+pub fn solar(y: usize, m: usize, d: usize) -> SexagenaryDate {
+    let jdf = JD!(y, m, d);
+    let jd = int(jdf);
+    let a = compute_ang(jdf);
+    let ygz = (y
+        - UNIT_YR
+        - ((m <= 2 && a < 316.) && (m < 2 || d < int(stday(2, y)))) as usize)
+        % 60;
+    let rem = a % 15.;
+    let div = int(a.div(15.).floor());
+    let mut dz = align((div + 1) / 2, 12, 9);
+    let mut termb = rem > 14.;
+    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 += termb as usize;
+            dz = align(dz, 12, 12);
+        }
+    }
+    let mut tmp = ygz.rem(5);
+    tmp = tmp * 2 + 2;
+    tmp -= 10 * (tmp == 10) as usize;
+    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
+pub fn dow(y: usize, mut m: usize, d: usize) -> (&'static str, &'static str) {
+    let Y = y - (m <= 2) as usize;
+    let y2 = Y % 100;
+    let c = Y / 100;
+    m += 9;
+    m -= 12 * (m >= 12) as usize;
+    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));
+        });
+    });
+}
-- 
cgit v1.2.3