//! 締め日・支払日の計算。
//! v2: 請求日が実在するか(形 YYYY-MM-DD・月 1〜12・日 1〜その月の日数)を確かめて invalid_date を返すようにし、春分日・秋分日の近似式を 10^-6 単位の整数で計算する。
//! v1 は split('-') で読んだ年月日をそのまま使ったので、平年の2月29日を受け付けて計算してしまった(標準ライブラリに日付の検証が無い)。
//! 年月日は整数の組 (y, m, d) のまま、曜日はツェラーの公式、1日進める・戻すは月の日数表で書く(v1 と同じ)。
use crate::json::{obj, s, Value};
use std::collections::HashSet;
type Date = (i64, i64, i64);
const WEEK: [&str; 7] = ["日", "月", "火", "水", "木", "金", "土"];
fn is_leap(y: i64) -> bool {
y % 4 == 0 && (y % 100 != 0 || y % 400 == 0)
}
fn days_in_month(y: i64, m: i64) -> i64 {
match m {
2 if is_leap(y) => 29,
2 => 28,
4 | 6 | 9 | 11 => 30,
_ => 31,
}
}
/// 曜日(0 = 日曜)。ツェラーの公式(h: 0 = 土曜)を日曜始まりに直す
fn weekday((y, m, d): Date) -> usize {
let (y, m) = if m < 3 { (y - 1, m + 12) } else { (y, m) };
let (k, j) = (y % 100, y / 100);
let h = (d + 13 * (m + 1) / 5 + k + k / 4 + j / 4 + 5 * j) % 7;
((h + 6) % 7) as usize
}
fn next_day((y, m, d): Date) -> Date {
if d < days_in_month(y, m) {
(y, m, d + 1)
} else if m < 12 {
(y, m + 1, 1)
} else {
(y + 1, 1, 1)
}
}
fn prev_day((y, m, d): Date) -> Date {
if d > 1 {
(y, m, d - 1)
} else if m > 1 {
(y, m - 1, days_in_month(y, m - 1))
} else {
(y - 1, 12, 31)
}
}
/// k か月後の年と月
fn add_months(y: i64, m: i64, k: i64) -> (i64, i64) {
let mut m = m + k;
let mut y = y;
while m > 12 {
m -= 12;
y += 1;
}
(y, m)
}
fn end_of_month(y: i64, m: i64) -> Date {
(y, m, days_in_month(y, m))
}
fn nth_monday(y: i64, m: i64, n: i64) -> Date {
let mut d = (y, m, 1);
while weekday(d) != 1 {
d = next_day(d);
}
(y, m, d.2 + 7 * (n - 1))
}
fn national_holidays(y: i64) -> HashSet<Date> {
// ⌊base + 0.242194 × (Y − 1980) − ⌊(Y − 1980) / 4⌋⌋ を 10^-6 単位の整数で
let k = y - 1980;
let equinox = |base_micro: i64| (base_micro + 242194 * k - 1_000_000 * k.div_euclid(4)).div_euclid(1_000_000);
let (spring, autumn) = (equinox(20_843_100), equinox(23_248_800));
let mut list = vec![
(y, 1, 1), nth_monday(y, 1, 2), (y, 2, 11), (y, 2, 23), (y, 3, spring), (y, 4, 29),
(y, 5, 3), (y, 5, 4), (y, 5, 5), nth_monday(y, 9, 3), (y, 9, autumn), (y, 11, 3), (y, 11, 23),
];
match y {
2020 => list.extend([(2020, 7, 23), (2020, 8, 10), (2020, 7, 24)]),
2021 => list.extend([(2021, 7, 22), (2021, 8, 8), (2021, 7, 23)]),
_ => list.extend([nth_monday(y, 7, 3), (y, 8, 11), nth_monday(y, 10, 2)]),
}
list.into_iter().collect()
}
fn holidays(y: i64) -> HashSet<Date> {
let national = national_holidays(y);
let mut rest = national.clone();
for &d in &national {
if weekday(d) == 0 {
// 振替休日
let mut x = next_day(d);
while national.contains(&x) {
x = next_day(x);
}
rest.insert(x);
}
}
let mut d = (y, 1, 2);
while d.0 == y {
// 国民の休日
if !national.contains(&d) && national.contains(&prev_day(d)) && national.contains(&next_day(d)) {
rest.insert(d);
}
d = next_day(d);
}
rest
}
fn is_business_day(d: Date) -> bool {
let w = weekday(d);
w != 0 && w != 6 && !holidays(d.0).contains(&d)
}
fn fmt((y, m, d): Date) -> String {
format!("{y:04}-{m:02}-{d:02}")
}
pub fn calc(date: &str, closing: &str, site: &str, adjust: &str) -> Result<Value, String> {
let parts: Vec<&str> = date.split('-').collect();
let well_formed = parts.len() == 3
&& parts.iter().zip([4, 2, 2]).all(|(p, w)| p.len() == w && p.bytes().all(|b| b.is_ascii_digit()));
if !well_formed {
return Err(format!("invalid date format: {date}"));
}
let (y, m, d) = (parts[0].parse::<i64>().unwrap(), parts[1].parse::<i64>().unwrap(), parts[2].parse::<i64>().unwrap());
if !(1..=12).contains(&m) || d < 1 || d > days_in_month(y, m) {
return Ok(obj(vec![("error", s("invalid_date"))]));
}
if y < 2020 || y > 2030 {
return Ok(obj(vec![("error", s("out_of_range"))]));
}
let close = if closing == "end" {
end_of_month(y, m)
} else {
let cd: i64 = closing.parse().map_err(|e| format!("{e}"))?;
if d <= cd {
(y, m, cd)
} else {
let (ny, nm) = add_months(y, m, 1);
(ny, nm, cd)
}
};
let pay = match site {
"this-end" => end_of_month(close.0, close.1),
"next-same" => {
let (ny, nm) = add_months(close.0, close.1, 1);
(ny, nm, close.2.min(days_in_month(ny, nm)))
}
"next-end" => {
let (ny, nm) = add_months(close.0, close.1, 1);
end_of_month(ny, nm)
}
_ => {
let (ny, nm) = add_months(close.0, close.1, 2);
end_of_month(ny, nm)
}
};
let mut adj = pay;
if adjust != "none" {
while !is_business_day(adj) {
adj = if adjust == "prev" { prev_day(adj) } else { next_day(adj) };
}
}
Ok(obj(vec![
("closing", s(fmt(close))),
("payment", s(fmt(pay))),
("adjusted", s(fmt(adj))),
("weekday", s(WEEK[weekday(adj)])),
]))
}