1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
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;
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},
];
// 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 * 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
}
#[inline(always)]
fn bisect<T>(
lo: &mut f64,
hi: &mut f64,
base: T,
scale: T,
target: f64,
y: usize,
m: f64,
) where
T: Into<f64> + 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<T>(a: T, to: T, zero: T) -> usize
where
T: cast::AsPrimitive<isize>,
{
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<T>(g: T, z: T) -> usize
where
T: cast::AsPrimitive<isize>,
{
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<f64> = 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<String> = {
const gan: [char; 10] = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'];
const zhi: [char; 12] = [ '子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉',
'戌', '亥', ];
let mut tmp: Vec<String> = 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
};
}
pub fn ganzhi(i: usize) -> &'static str {
&GANZHIS[i]
}
#[inline(always)]
fn int(x: f64) -> usize {
x as usize // x.as_int_unchecked() // wont work on wasm
}
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));
});
});
}
|