-
Notifications
You must be signed in to change notification settings - Fork 195
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Сalculation speed up for the Gregorian calendar #5849
Open
Nikita-str
wants to merge
16
commits into
unicode-org:main
Choose a base branch
from
Nikita-str:calenda-calc-speed-up
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,782
−249
Open
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
238d53f
#5562: more fast impl of Gregorian calendar calc
Nikita-str 81d26ed
cargo fmt & remove branching
Nikita-str 1f23401
#5562: tests that new impl return the same values as old impl
Nikita-str dfa534c
make clippy happy
Nikita-str a632f4c
`const fn` & typo & LICENSE:TODO & test for 2nd approx
Nikita-str 4758647
impl fast `day_of_week` & another impl for `month_days`
Nikita-str e3ddbd6
new way to calc `iso_from_year_day`
Nikita-str 8404d4d
new way to calc `Iso::day_of_year`
Nikita-str 6bb310b
Gregorian: some refactoring
Nikita-str 06df27a
Julian calendar perf : `Cassio Neri & Lorenz Schneider` algo
Nikita-str c73a12b
Julian calendar: micro change
Nikita-str 28f1a2c
some tests
Nikita-str 0ba625a
Gregorian calc: slightly speed up
Nikita-str 9ff91e4
Gregorian calendar benches
Nikita-str 0d46522
Julian calendar calc benches
Nikita-str a85f86a
codestyle & comment fixes
Nikita-str File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,291 @@ | ||
use core::hint::black_box as bb; | ||
|
||
use calendrical_calculations::{helpers::I32CastError, rata_die::RataDie}; | ||
use criterion::{criterion_group, criterion_main, Criterion}; | ||
|
||
#[cfg(feature = "bench")] | ||
use calendrical_calculations::bench_support::iso_old as old; | ||
use calendrical_calculations::iso as new; | ||
|
||
#[cfg(feature = "bench")] | ||
use calendrical_calculations::bench_support::julian_old as j_old; | ||
use calendrical_calculations::julian as j_new; | ||
|
||
const MONTH_DAYS: [u8; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; | ||
type IsoFromFixedFn = fn(RataDie) -> Result<(i32, u8, u8), I32CastError>; | ||
|
||
fn prep_gen_ymd_vec(y: i32, delta: i32) -> Vec<(i32, u8, u8)> { | ||
let mut ret = Vec::with_capacity(delta as usize * 2 * 366); | ||
for year in y - delta..=y + delta { | ||
for month in 1..=12u8 { | ||
for day in 1..=MONTH_DAYS[(month as usize) - 1] { | ||
ret.push((year, month, day)); | ||
} | ||
} | ||
} | ||
bb(ret) | ||
} | ||
|
||
fn prep_gen_yd_vec(y: i32, delta: i32, from: u16, to: u16) -> Vec<(i32, u16)> { | ||
let mut ret = Vec::with_capacity(delta as usize * 2 * 365); | ||
for year in y - delta..=y + delta { | ||
for day in from..=to { | ||
ret.push((year, day)) | ||
} | ||
} | ||
bb(ret) | ||
} | ||
|
||
fn prep_gen_rata_die_vec(n: i64) -> Vec<RataDie> { | ||
let mut ret = Vec::with_capacity(n as usize); | ||
(1..=n).for_each(|n| ret.push(RataDie::new(n))); | ||
bb(ret) | ||
} | ||
|
||
fn fixed_from(f: fn(i32, u8, u8) -> RataDie, ymd_vec: &Vec<(i32, u8, u8)>) -> i64 { | ||
// The problem here is that LTO(it is turned on for benches in the workspace) | ||
// is very good at cyclic optimization when we goes year by year: | ||
// ``` | ||
// for year in (y - delta)..=(y + delta) { | ||
// for month in 1..=12u8 { | ||
// for day in 1..=MONTH_DAYS[(month as usize) - 1] { | ||
// ... | ||
// ``` | ||
// and seems like the optimizer find out that in such cycles | ||
// result will be differ a little, and loop unroling stage | ||
// transforms it in something very optimized | ||
// (like incrimenting by one. It seems really so, because for new algo | ||
// mentioned cycle was optimized to perf of empty cycle with `black_box`'es) | ||
// | ||
// And we want to test perf of the algos, not how good the optimizer work with | ||
// cases when the dates goes one by one. | ||
// | ||
// And there we get another problem: the algo(at least the new one) is +- as fast | ||
// as {cycle + `black_box`} overheads :// | ||
// So we should to call the function multiple times to at least reduce | ||
// the influence of perf measurements of the cycle ¯\_(ツ)_/¯ | ||
|
||
let mut sum = 0; | ||
for (year, month, day) in ymd_vec { | ||
let (year, month, day) = (bb(*year), bb(*month), bb(*day)); | ||
|
||
// If you comment next lines | ||
// Then you will get ~10% of NEW algo (lets say this ~10% is `X ms`) | ||
// So real perf ratio is: | ||
// `(OLD - X)/(NEW - X)` | ||
// And in my case this is +- about the asm instr len differ | ||
sum += f(year, month, day).to_i64_date(); | ||
sum += f(year + 7, month, (day + 2) >> 1).to_i64_date(); | ||
sum += f(year + 37, (month + 3) >> 1, (day + 3) >> 1).to_i64_date(); | ||
sum += f(year + 137, (month + 7) >> 1, (day + 19) >> 1).to_i64_date(); | ||
} | ||
|
||
bb(sum) | ||
} | ||
|
||
fn day_of_week(f: fn(i32, u8, u8) -> u8, ymd_vec: &Vec<(i32, u8, u8)>) -> i32 { | ||
let mut sum = 0; | ||
for (year, month, day) in ymd_vec { | ||
let (year, month, day) = (bb(*year), bb(*month), bb(*day)); | ||
|
||
sum += f(year, month, day) as i32; | ||
sum += f(year + 31, month, (day + 7) >> 1) as i32; | ||
sum += f(year + 141, (month + 5) >> 1, day) as i32; | ||
sum += f(year + 243, (month + 2) >> 1, (day + 17) >> 1) as i32; | ||
} | ||
|
||
sum | ||
} | ||
|
||
fn day_of_year(f: fn(i32, u8, u8) -> u16, ymd_vec: &Vec<(i32, u8, u8)>) -> i32 { | ||
let mut sum = 0; | ||
for (year, month, day) in ymd_vec { | ||
let (year, month, day) = (bb(*year), bb(*month), bb(*day)); | ||
|
||
sum += f(year, month, day) as i32; | ||
sum += f(year + 31, month, (day + 7) >> 1) as i32; | ||
sum += f(year + 141, (month + 5) >> 1, day) as i32; | ||
sum += f(year + 243, (month + 2) >> 1, (day + 17) >> 1) as i32; | ||
} | ||
|
||
sum | ||
} | ||
|
||
fn from_fixed(f: IsoFromFixedFn, rd_vec: &Vec<RataDie>) -> i64 { | ||
let map = |r: Result<(i32, u8, u8), I32CastError>| { | ||
let x = r.unwrap(); | ||
x.0 as i64 + (x.1 + x.2) as i64 | ||
}; | ||
|
||
let mut sum = 0; | ||
for rd in rd_vec { | ||
let rd = bb(*rd).to_i64_date(); | ||
|
||
sum += map(f(RataDie::new(rd))); | ||
sum += map(f(RataDie::new(rd + 1313))); | ||
sum += map(f(RataDie::new(rd + 7429))); | ||
sum += map(f(RataDie::new(rd - 5621))); | ||
} | ||
|
||
sum | ||
} | ||
|
||
fn iso_from_year_day(f: fn(i32, u16) -> (u8, u8), rd_vec: &Vec<(i32, u16)>) -> i64 { | ||
let map = |x: (u8, u8)| (x.0 + x.1) as i64; | ||
let mut sum = 0; | ||
|
||
for (year, day_of_year) in rd_vec { | ||
let (year, day_of_year) = (bb(*year), bb(*day_of_year)); | ||
|
||
sum += map(f(year, day_of_year)); | ||
sum += map(f(year + 21, day_of_year)); | ||
sum += map(f(year + 97, (day_of_year + 127) >> 1)); | ||
sum += map(f(year + 137, (day_of_year + 12) >> 1)); | ||
} | ||
|
||
sum | ||
} | ||
|
||
#[no_mangle] | ||
fn bench_fixed_from(c: &mut Criterion) { | ||
const Y: i32 = 1_000; | ||
const DELTA: i32 = 2_000; | ||
let ymd_vec = prep_gen_ymd_vec(Y, DELTA); | ||
|
||
{ | ||
let mut group = c.benchmark_group("fixed_from_iso"); | ||
|
||
#[cfg(feature = "bench")] | ||
group.bench_function("OLD", |b| { | ||
b.iter(|| fixed_from(old::fixed_from_iso, bb(&ymd_vec))) | ||
}); | ||
group.bench_function("NEW", |b| { | ||
b.iter(|| fixed_from(new::fixed_from_iso, bb(&ymd_vec))) | ||
}); | ||
} | ||
|
||
{ | ||
let mut group = c.benchmark_group("fixed_from_julian"); | ||
|
||
#[cfg(feature = "bench")] | ||
group.bench_function("OLD", |b| { | ||
b.iter(|| fixed_from(j_old::fixed_from_julian, bb(&ymd_vec))) | ||
}); | ||
group.bench_function("NEW", |b| { | ||
b.iter(|| fixed_from(j_new::fixed_from_julian, bb(&ymd_vec))) | ||
}); | ||
} | ||
} | ||
|
||
fn bench_day_of_week(c: &mut Criterion) { | ||
const Y: i32 = 1_000; | ||
const DELTA: i32 = 2_000; | ||
let ymd_vec = prep_gen_ymd_vec(Y, DELTA); | ||
|
||
let mut group = c.benchmark_group("day_of_week"); | ||
|
||
#[cfg(feature = "bench")] | ||
group.bench_function("iso/OLD", |b| { | ||
b.iter(|| day_of_week(old::day_of_week, bb(&ymd_vec))) | ||
}); | ||
group.bench_function("iso/NEW", |b| { | ||
b.iter(|| day_of_week(new::day_of_week, bb(&ymd_vec))) | ||
}); | ||
} | ||
|
||
fn bench_day_of_year(c: &mut Criterion) { | ||
const Y: i32 = 1_000; | ||
const DELTA: i32 = 2_000; | ||
let ymd_vec = prep_gen_ymd_vec(Y, DELTA); | ||
|
||
let mut group = c.benchmark_group("day_of_year"); | ||
|
||
#[cfg(feature = "bench")] | ||
group.bench_function("iso/OLD", |b| { | ||
b.iter(|| day_of_year(old::day_of_year, bb(&ymd_vec))) | ||
}); | ||
group.bench_function("iso/NEW", |b| { | ||
b.iter(|| day_of_year(new::day_of_year, bb(&ymd_vec))) | ||
}); | ||
|
||
#[cfg(feature = "bench")] | ||
group.bench_function("julian/OLD", |b| { | ||
b.iter(|| day_of_year(j_old::day_of_year, bb(&ymd_vec))) | ||
}); | ||
group.bench_function("julian/NEW", |b| { | ||
b.iter(|| day_of_year(j_new::day_of_year, bb(&ymd_vec))) | ||
}); | ||
} | ||
|
||
fn bench_from_fixed(c: &mut Criterion) { | ||
const N: i64 = 10_000; | ||
let rd_vec = prep_gen_rata_die_vec(N); | ||
|
||
let mut group = c.benchmark_group("from_fixed"); | ||
|
||
#[cfg(feature = "bench")] | ||
group.bench_function("iso/OLD", |b| { | ||
b.iter(|| from_fixed(old::iso_from_fixed, bb(&rd_vec))) | ||
}); | ||
group.bench_function("iso/NEW", |b| { | ||
b.iter(|| from_fixed(new::iso_from_fixed, bb(&rd_vec))) | ||
}); | ||
|
||
#[cfg(feature = "bench")] | ||
group.bench_function("julian/OLD", |b| { | ||
b.iter(|| from_fixed(j_old::julian_from_fixed, bb(&rd_vec))) | ||
}); | ||
group.bench_function("julian/NEW", |b| { | ||
b.iter(|| from_fixed(j_new::julian_from_fixed, bb(&rd_vec))) | ||
}); | ||
} | ||
|
||
fn bench_iso_from_year_day(c: &mut Criterion) { | ||
const Y: i32 = 1_000; | ||
const DELTA: i32 = 2_000; | ||
|
||
let mut group = c.benchmark_group("from_year_day"); | ||
|
||
let yd_vec = prep_gen_yd_vec(Y, DELTA, 1, 365); | ||
#[cfg(feature = "bench")] | ||
group.bench_function("iso/OLD/AVG", |b| { | ||
b.iter(|| iso_from_year_day(old::iso_from_year_day, bb(&yd_vec))) | ||
}); | ||
group.bench_function("iso/NEW/AVG", |b| { | ||
b.iter(|| iso_from_year_day(new::iso_from_year_day, bb(&yd_vec))) | ||
}); | ||
|
||
// In range of first two months old algo is faster | ||
// And they ~ the same perf in 3rd/4th months | ||
let yd_vec = prep_gen_yd_vec(Y, DELTA, 3, 57); | ||
#[cfg(feature = "bench")] | ||
group.bench_function("iso/OLD/START", |b| { | ||
b.iter(|| iso_from_year_day(old::iso_from_year_day, bb(&yd_vec))) | ||
}); | ||
group.bench_function("iso/NEW/START", |b| { | ||
b.iter(|| iso_from_year_day(new::iso_from_year_day, bb(&yd_vec))) | ||
}); | ||
|
||
let yd_vec = prep_gen_yd_vec(Y, DELTA, 300, 360); | ||
#[cfg(feature = "bench")] | ||
group.bench_function("iso/OLD/END", |b| { | ||
b.iter(|| iso_from_year_day(old::iso_from_year_day, bb(&yd_vec))) | ||
}); | ||
group.bench_function("iso/NEW/END", |b| { | ||
b.iter(|| iso_from_year_day(new::iso_from_year_day, bb(&yd_vec))) | ||
}); | ||
} | ||
|
||
criterion_group!(benchmark_fixed_from, bench_fixed_from); | ||
criterion_group!(benchmark_year_from_fixed, bench_day_of_week); | ||
criterion_group!(benchmark_day_of_year, bench_day_of_year); | ||
criterion_group!(benchmark_from_fixed, bench_from_fixed); | ||
criterion_group!(benchmark_iso_from_year_day, bench_iso_from_year_day); | ||
|
||
criterion_main!( | ||
benchmark_fixed_from, | ||
benchmark_year_from_fixed, | ||
benchmark_day_of_year, | ||
benchmark_from_fixed, | ||
benchmark_iso_from_year_day | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,297 @@ | ||
use super::helpful_consts::*; | ||
use super::iso_old_algos as alg_old; | ||
use super::iso_old_file as iso_old; | ||
use crate::iso as iso_new; | ||
use crate::rata_die::RataDie; | ||
use core::ops::RangeInclusive; | ||
|
||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
// [+] helpful fns | ||
|
||
fn calc_last_month_day(year: i32, month: u8) -> u8 { | ||
let to = MONTH_DAYS[(month as usize) - 1]; | ||
if iso_old::is_leap_year(year) && (month == 2) { | ||
to + 1 | ||
} else { | ||
to | ||
} | ||
} | ||
|
||
pub(crate) fn assert_year(year: i32, mut assert_f: impl FnMut(i32, u8, u8)) { | ||
for month in 1..=12u8 { | ||
for day in 1..=calc_last_month_day(year, month) { | ||
assert_f(year, month, day) | ||
} | ||
} | ||
} | ||
|
||
fn assert_year_rata_die(year: i32, mut assert_f: impl FnMut(i32, u8, u8, RataDie)) { | ||
for month in 1..=12u8 { | ||
for day in 1..=calc_last_month_day(year, month) { | ||
let rata_die = iso_old::fixed_from_iso(year, month, day); | ||
assert_f(year, month, day, rata_die) | ||
} | ||
} | ||
} | ||
|
||
// [-] helpful fns | ||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
|
||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
// [+] test correctness | ||
|
||
#[test] | ||
fn test_algo_correctness() { | ||
// 1st Jan of 1 year: | ||
let rata_die_initial = RataDie::new(1); | ||
|
||
// Now we know that `iso_new::iso_from_fixed` is correct for YMD(1, 1, 1): | ||
assert_eq!(iso_new::iso_from_fixed(rata_die_initial), Ok((1, 1, 1))); | ||
// Now we know that `iso_new::day_of_week` is correct for YMD(2024, 11, 25): | ||
assert_eq!(iso_new::day_of_week(2024, 11, 25), 1); | ||
|
||
let delta = 9_999; // 3_999_999; | ||
|
||
// This can be incorrect, but if it so, then on YMD(1, 1, 1) we will figured that out, | ||
// because if it is incorrect we get incorrect value for `rata_die_i64` for the YMD(1, 1, 1) | ||
// and `assert_eq!(iso_new::iso_from_fixed(rata_die), Ok((year, month, day)))` will panic | ||
let mut rata_die_i64 = iso_new::fixed_from_iso(-delta, 1, 1).to_i64_date(); | ||
|
||
// This can be incorrect, but if it so, then on YMD(2024, 11, 25) we will figured that out | ||
// (days of weeks goes cyclic on each other) | ||
let mut day_of_week = iso_new::day_of_week(2024, 11, 25); | ||
|
||
let mut year_day = 1; | ||
// Because rata die's stay each after each we can verify that in a range | ||
// all of `iso_new::iso_from_fixed` is correct, if that range contains the YMD(1, 1, 1): | ||
for year in -delta..=delta { | ||
assert_year(year, |year, month, day| { | ||
if (month == 1) && (day == 1) { | ||
year_day = 1; | ||
} | ||
|
||
let rata_die = RataDie::new(rata_die_i64); | ||
assert_eq!(iso_new::iso_from_fixed(rata_die), Ok((year, month, day))); | ||
assert_eq!(iso_new::iso_year_from_fixed(rata_die), year as i64); | ||
assert_eq!(iso_new::fixed_from_iso(year, month, day), rata_die); | ||
assert_eq!(iso_new::day_of_week(year, month, day), day_of_week); | ||
|
||
assert_eq!(iso_new::iso_from_year_day(year, year_day), (month, day)); | ||
assert_eq!(iso_new::day_of_year(year, month, day), year_day); | ||
|
||
if (month == 12) && (day == 31) { | ||
assert_eq!( | ||
year_day, | ||
365 + iso_old::is_leap_year(year) as u16, | ||
"YMD: {year} {month} {day}" | ||
); | ||
} | ||
|
||
rata_die_i64 += 1; | ||
|
||
day_of_week += 1; | ||
if day_of_week == 8 { | ||
day_of_week = 1; | ||
} | ||
|
||
year_day += 1; | ||
}); | ||
} | ||
|
||
// FNS: {iso_from_fixed, iso_year_from_fixed, fixed_from_iso, day_of_week, iso_from_year_day, day_of_year}; | ||
// So now we sure that all fns from FNS is correct (at least in range -delta..=delta) | ||
} | ||
|
||
// [-] test correctness | ||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
|
||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
// [+] test the same result as prev algo | ||
|
||
#[test] | ||
fn test_is_leap_year_the_same() { | ||
const N_YEAR: i32 = 99_999; | ||
const N_RANGE: RangeInclusive<i32> = (-N_YEAR)..=N_YEAR; // i32::MIN..=i32::MAX | ||
|
||
fn the_same_in_year(year: i32) { | ||
assert_eq!( | ||
iso_old::is_leap_year(year), | ||
iso_new::is_leap_year(year), | ||
"year = {year}" | ||
); | ||
} | ||
|
||
N_RANGE.for_each(the_same_in_year); | ||
MIN_YEAR_BOUND_RANGE.for_each(the_same_in_year); | ||
MAX_YEAR_BOUND_RANGE.for_each(the_same_in_year); | ||
} | ||
|
||
#[test] | ||
fn test_fixed_from_iso_the_same() { | ||
const N_YEAR: i32 = 9_999; | ||
const N_RANGE: RangeInclusive<i32> = (-N_YEAR)..=N_YEAR; | ||
|
||
let the_same_in_year = |year| { | ||
assert_year(year, |year, month, day| { | ||
assert_eq!( | ||
iso_old::fixed_from_iso(year, month, day), | ||
iso_new::fixed_from_iso(year, month, day), | ||
"YMD: {year} {month} {day}" | ||
); | ||
}) | ||
}; | ||
|
||
N_RANGE.for_each(the_same_in_year); | ||
MIN_YEAR_BOUND_RANGE.for_each(the_same_in_year); | ||
MAX_YEAR_BOUND_RANGE.for_each(the_same_in_year); | ||
} | ||
|
||
#[test] | ||
fn test_year_from_fixed_the_same() { | ||
const N_YEAR: i32 = 9_999; | ||
const N_RANGE: RangeInclusive<i32> = (-N_YEAR)..=N_YEAR; | ||
|
||
let the_same_in_year = |year| { | ||
assert_year_rata_die(year, |year, month, day, rata_die| { | ||
assert_eq!( | ||
year as i64, | ||
iso_new::iso_year_from_fixed(rata_die), | ||
"{year} {month} {day} | {rata_die:?}" | ||
); | ||
}) | ||
}; | ||
|
||
N_RANGE.for_each(the_same_in_year); | ||
MIN_YEAR_BOUND_RANGE.for_each(the_same_in_year); | ||
MAX_YEAR_BOUND_RANGE.for_each(the_same_in_year); | ||
|
||
const RATA_MIN: i64 = i64::MIN / 256; | ||
const RATA_MAX: i64 = i64::MAX / 256; | ||
const N_DAYS: i64 = 9_999; | ||
|
||
for rata_die in RATA_MIN..(RATA_MIN + N_DAYS) { | ||
let rata_die = RataDie::new(rata_die); | ||
assert_eq!( | ||
iso_old::iso_year_from_fixed(rata_die), | ||
iso_new::iso_year_from_fixed(rata_die), | ||
"{rata_die:?}" | ||
); | ||
} | ||
for rata_die in (RATA_MAX - N_DAYS)..=RATA_MAX { | ||
let rata_die = RataDie::new(rata_die); | ||
assert_eq!( | ||
iso_old::iso_year_from_fixed(rata_die), | ||
iso_new::iso_year_from_fixed(rata_die), | ||
"{rata_die:?}" | ||
); | ||
} | ||
} | ||
|
||
#[test] | ||
fn test_from_fixed_eq() { | ||
const N_YEAR: i32 = 1_999; // `iso_old::iso_from_fixed` is slow | ||
const N_RANGE: RangeInclusive<i32> = (-N_YEAR)..=N_YEAR; | ||
|
||
let the_same_in_year = |year| { | ||
assert_year_rata_die(year, |year, month, day, rata_die| { | ||
assert_eq!( | ||
iso_old::iso_from_fixed(rata_die), | ||
iso_new::iso_from_fixed(rata_die), | ||
"{year} {month} {day}" | ||
); | ||
}) | ||
}; | ||
|
||
N_RANGE.for_each(the_same_in_year); | ||
MIN_YEAR_BOUND_RANGE.for_each(the_same_in_year); | ||
MAX_YEAR_BOUND_RANGE.for_each(the_same_in_year); | ||
|
||
const RATA_MIN: i64 = i64::MIN / 256; | ||
const RATA_MAX: i64 = i64::MAX / 256; | ||
const N_DAYS: i64 = 99_999; | ||
|
||
for rata_die in RATA_MIN..(RATA_MIN + N_DAYS) { | ||
let rata_die = RataDie::new(rata_die); | ||
assert_eq!( | ||
iso_old::iso_from_fixed(rata_die), | ||
iso_new::iso_from_fixed(rata_die), | ||
"{rata_die:?}" | ||
); | ||
} | ||
for rata_die in (RATA_MAX - N_DAYS)..=RATA_MAX { | ||
let rata_die = RataDie::new(rata_die); | ||
assert_eq!( | ||
iso_old::iso_from_fixed(rata_die), | ||
iso_new::iso_from_fixed(rata_die), | ||
"{rata_die:?}" | ||
); | ||
} | ||
} | ||
|
||
// New algos in the `iso` and prev. algos from others files: | ||
|
||
#[test] | ||
fn test_day_of_week_the_same() { | ||
const N_YEAR: i32 = 2_999; | ||
const N_RANGE: RangeInclusive<i32> = (-N_YEAR)..=N_YEAR; | ||
|
||
fn the_same_in_year(year: i32) { | ||
for month in 1..=12u8 { | ||
for day in 1..=calc_last_month_day(year, month) { | ||
assert_eq!( | ||
alg_old::day_of_week(year, month, day), | ||
iso_new::day_of_week(year, month, day), | ||
"YMD: {year} {month} {day}" | ||
); | ||
} | ||
} | ||
} | ||
|
||
N_RANGE.for_each(the_same_in_year); | ||
MIN_YEAR_BOUND_RANGE.for_each(the_same_in_year); | ||
MAX_YEAR_BOUND_RANGE.for_each(the_same_in_year); | ||
} | ||
|
||
#[test] | ||
fn test_from_year_day_the_same() { | ||
const N_YEAR: i32 = 2_999; | ||
const N_RANGE: RangeInclusive<i32> = (-N_YEAR)..=N_YEAR; | ||
|
||
fn the_same_in_year(year: i32) { | ||
let days = 365 + iso_new::is_leap_year(year) as u16; | ||
for day_of_year in 1..=days { | ||
assert_eq!( | ||
alg_old::iso_from_year_day(year, day_of_year), | ||
iso_new::iso_from_year_day(year, day_of_year), | ||
"Y & DofY: {year} {day_of_year}" | ||
); | ||
} | ||
} | ||
|
||
N_RANGE.for_each(the_same_in_year); | ||
MIN_YEAR_BOUND_RANGE.for_each(the_same_in_year); | ||
MAX_YEAR_BOUND_RANGE.for_each(the_same_in_year); | ||
} | ||
|
||
#[test] | ||
fn test_day_of_year_the_same() { | ||
const N_YEAR: i32 = 2_999; | ||
const N_RANGE: RangeInclusive<i32> = (-N_YEAR)..=N_YEAR; | ||
|
||
let the_same_in_year = |year| { | ||
assert_year(year, |year, month, day| { | ||
assert_eq!( | ||
alg_old::day_of_year(year, month, day), | ||
iso_new::day_of_year(year, month, day), | ||
"YMD: {year} {month} {day}" | ||
); | ||
}) | ||
}; | ||
|
||
N_RANGE.for_each(the_same_in_year); | ||
MIN_YEAR_BOUND_RANGE.for_each(the_same_in_year); | ||
MAX_YEAR_BOUND_RANGE.for_each(the_same_in_year); | ||
} | ||
|
||
// [-] test the same result as prev algo | ||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
102 changes: 102 additions & 0 deletions
102
utils/calendrical_calculations/src/tests/iso_old_algos.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
use crate::iso::is_leap_year; | ||
|
||
type IsoWeekday = u8; | ||
|
||
/// Prev algo from: `components\calendar\src\iso.rs` | ||
// In the code removed: `date.0.` | ||
// Next line WAS: `fn day_of_week(&self, date: &Self::DateInner) -> types::IsoWeekday {` | ||
pub fn day_of_week(year: i32, month: u8, day: u8) -> IsoWeekday { | ||
// For the purposes of the calculation here, Monday is 0, Sunday is 6 | ||
// ISO has Monday=1, Sunday=7, which we transform in the last step | ||
|
||
// The days of the week are the same every 400 years | ||
// so we normalize to the nearest multiple of 400 | ||
let years_since_400 = year.rem_euclid(400); | ||
debug_assert!(years_since_400 >= 0); // rem_euclid returns positive numbers | ||
let years_since_400 = years_since_400 as u32; | ||
let leap_years_since_400 = years_since_400 / 4 - years_since_400 / 100; | ||
// The number of days to the current year | ||
// Can never cause an overflow because years_since_400 has a maximum value of 399. | ||
let days_to_current_year = 365 * years_since_400 + leap_years_since_400; | ||
// The weekday offset from January 1 this year and January 1 2000 | ||
let year_offset = days_to_current_year % 7; | ||
|
||
// Corresponding months from | ||
// https://en.wikipedia.org/wiki/Determination_of_the_day_of_the_week#Corresponding_months | ||
let month_offset = if is_leap_year(year) { | ||
match month { | ||
10 => 0, | ||
5 => 1, | ||
2 | 8 => 2, | ||
3 | 11 => 3, | ||
6 => 4, | ||
9 | 12 => 5, | ||
1 | 4 | 7 => 6, | ||
_ => unreachable!(), | ||
} | ||
} else { | ||
match month { | ||
1 | 10 => 0, | ||
5 => 1, | ||
8 => 2, | ||
2 | 3 | 11 => 3, | ||
6 => 4, | ||
9 | 12 => 5, | ||
4 | 7 => 6, | ||
_ => unreachable!(), | ||
} | ||
}; | ||
let january_1_2000 = 5; // Saturday | ||
let day_offset = (january_1_2000 + year_offset + month_offset + day as u32) % 7; | ||
|
||
// We calculated in a zero-indexed fashion, but ISO specifies one-indexed | ||
(day_offset + 1) as u8 | ||
} | ||
|
||
/// Count the number of days in a given month/year combo | ||
const fn days_in_month(year: i32, month: u8) -> u8 { | ||
// see comment to `<impl CalendarArithmetic for Iso>::month_days` | ||
match month { | ||
2 => 28 | (is_leap_year(year) as u8), | ||
_ => 30 | (month ^ (month >> 3)), | ||
} | ||
} | ||
|
||
/// Prev algo from: `components\calendar\src\iso.rs::Iso` | ||
/// | ||
/// Return `(day, month)` for the given `year & `day_of_year` | ||
pub fn iso_from_year_day(year: i32, year_day: u16) -> (u8, u8) { | ||
let mut month = 1; | ||
let mut day = year_day as i32; | ||
while month <= 12 { | ||
let month_days = days_in_month(year, month) as i32; | ||
if day <= month_days { | ||
break; | ||
} else { | ||
debug_assert!(month < 12); // don't try going to month 13 | ||
day -= month_days; | ||
month += 1; | ||
} | ||
} | ||
let day = day as u8; // day <= month_days < u8::MAX | ||
|
||
(month, day) | ||
} | ||
|
||
/// Prev algo from: `components\calendar\src\iso.rs::Iso` | ||
/// | ||
/// Return `day_of_the_year` (`1..=365`/`1..=366`) | ||
pub fn day_of_year(year: i32, month: u8, day: u8) -> u16 { | ||
// Cumulatively how much are dates in each month | ||
// offset from "30 days in each month" (in non leap years) | ||
let month_offset = [0, 1, -1, 0, 0, 1, 1, 2, 3, 3, 4, 4]; | ||
#[allow(clippy::indexing_slicing)] // date.0.month in 1..=12 | ||
let mut offset = month_offset[month as usize - 1]; | ||
if is_leap_year(year) && month > 2 { | ||
// Months after February in a leap year are offset by one less | ||
offset += 1; | ||
} | ||
let prev_month_days = (30 * (month as i32 - 1) + offset) as u16; | ||
|
||
prev_month_days + day as u16 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
// This file is part of ICU4X. | ||
// | ||
// The contents of this file implement algorithms from Calendrical Calculations | ||
// by Reingold & Dershowitz, Cambridge University Press, 4th edition (2018), | ||
// which have been released as Lisp code at <https://github.com/EdReingold/calendar-code2/> | ||
// under the Apache-2.0 license. Accordingly, this file is released under | ||
// the Apache License, Version 2.0 which can be found at the calendrical_calculations | ||
// package root or at http://www.apache.org/licenses/LICENSE-2.0. | ||
|
||
use crate::helpers::{i64_to_i32, I32CastError}; | ||
use crate::rata_die::RataDie; | ||
|
||
// The Gregorian epoch is equivalent to first day in fixed day measurement | ||
const EPOCH: RataDie = RataDie::new(1); | ||
|
||
/// Whether or not `year` is a leap year | ||
#[inline] // real call will be more complex operation than inner code | ||
pub fn is_leap_year(year: i32) -> bool { | ||
year % 4 == 0 && (year % 400 == 0 || year % 100 != 0) | ||
} | ||
|
||
// Fixed is day count representation of calendars starting from Jan 1st of year 1. | ||
// The fixed calculations algorithms are from the Calendrical Calculations book. | ||
// | ||
/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/1ee51ecfaae6f856b0d7de3e36e9042100b4f424/calendar.l#L1167-L1189> | ||
pub fn fixed_from_iso(year: i32, month: u8, day: u8) -> RataDie { | ||
let prev_year = (year as i64) - 1; | ||
// Calculate days per year | ||
let mut fixed: i64 = (EPOCH.to_i64_date() - 1) + 365 * prev_year; | ||
// Calculate leap year offset | ||
let offset = prev_year.div_euclid(4) - prev_year.div_euclid(100) + prev_year.div_euclid(400); | ||
// Adjust for leap year logic | ||
fixed += offset; | ||
// Days of current year | ||
fixed += (367 * (month as i64) - 362).div_euclid(12); | ||
// Leap year adjustment for the current year | ||
fixed += if month <= 2 { | ||
0 | ||
} else if is_leap_year(year) { | ||
-1 | ||
} else { | ||
-2 | ||
}; | ||
// Days passed in current month | ||
fixed += day as i64; | ||
RataDie::new(fixed) | ||
} | ||
|
||
/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/1ee51ecfaae6f856b0d7de3e36e9042100b4f424/calendar.l#L1191-L1217> | ||
pub(crate) fn iso_year_from_fixed(date: RataDie) -> i64 { | ||
// Shouldn't overflow because it's not possbile to construct extreme values of RataDie | ||
let date = date - EPOCH; | ||
|
||
// 400 year cycles have 146097 days | ||
let (n_400, date) = (date.div_euclid(146097), date.rem_euclid(146097)); | ||
|
||
// 100 year cycles have 36524 days | ||
let (n_100, date) = (date.div_euclid(36524), date.rem_euclid(36524)); | ||
|
||
// 4 year cycles have 1461 days | ||
let (n_4, date) = (date.div_euclid(1461), date.rem_euclid(1461)); | ||
|
||
let n_1 = date.div_euclid(365); | ||
|
||
let year = 400 * n_400 + 100 * n_100 + 4 * n_4 + n_1; | ||
|
||
if n_100 == 4 || n_1 == 4 { | ||
year | ||
} else { | ||
year + 1 | ||
} | ||
} | ||
|
||
fn iso_new_year(year: i32) -> RataDie { | ||
fixed_from_iso(year, 1, 1) | ||
} | ||
|
||
/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/1ee51ecfaae6f856b0d7de3e36e9042100b4f424/calendar.l#L1525-L1540> | ||
pub fn iso_from_fixed(date: RataDie) -> Result<(i32, u8, u8), I32CastError> { | ||
let year = iso_year_from_fixed(date); | ||
let year = i64_to_i32(year)?; | ||
// Calculates the prior days of the adjusted year, then applies a correction based on leap year conditions for the correct ISO date conversion. | ||
let prior_days = date - iso_new_year(year); | ||
let correction = if date < fixed_from_iso(year, 3, 1) { | ||
0 | ||
} else if is_leap_year(year) { | ||
1 | ||
} else { | ||
2 | ||
}; | ||
let month = (12 * (prior_days + correction) + 373).div_euclid(367) as u8; // in 1..12 < u8::MAX | ||
let day = (date - fixed_from_iso(year, month, 1) + 1) as u8; // <= days_in_month < u8::MAX | ||
Ok((year, month, day)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,190 @@ | ||
use super::helpful_consts::*; | ||
use super::julian_old_file as old; | ||
use crate::{helpers::I32CastError, julian as new, rata_die::RataDie}; | ||
use core::ops::RangeInclusive; | ||
|
||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
// [+] helpful fns | ||
|
||
fn calc_last_month_day(year: i32, month: u8) -> u8 { | ||
let to = MONTH_DAYS[(month as usize) - 1]; | ||
if new::is_leap_year(year) && (month == 2) { | ||
to + 1 | ||
} else { | ||
to | ||
} | ||
} | ||
|
||
pub(crate) fn assert_year(year: i32, mut assert_f: impl FnMut(i32, u8, u8)) { | ||
for month in 1..=12u8 { | ||
for day in 1..=calc_last_month_day(year, month) { | ||
assert_f(year, month, day) | ||
} | ||
} | ||
} | ||
|
||
fn assert_year_rata_die(year: i32, mut assert_f: impl FnMut(i32, u8, u8, RataDie)) { | ||
for month in 1..=12u8 { | ||
for day in 1..=calc_last_month_day(year, month) { | ||
let rata_die = old::fixed_from_julian(year, month, day); | ||
assert_f(year, month, day, rata_die) | ||
} | ||
} | ||
} | ||
|
||
// [-] helpful fns | ||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
|
||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
// [+] test correctness | ||
|
||
#[test] | ||
fn test_algo_correctness() { | ||
// In old algo there was a comment: | ||
// ``` | ||
// Julian epoch is equivalent to fixed_from_iso of December 30th of 0 year | ||
// 1st Jan of 1st year Julian is equivalent to December 30th of 0th year of ISO year | ||
// ``` | ||
// ⚠️ IS IT TRUE? ⚠️ | ||
|
||
// firstly we must be sure that `crate::iso::fixed_from_iso` is correct | ||
// (see `super::iso::test_algo_correctness`) | ||
// | ||
// 1st Jan of 1 year: | ||
let rata_die_initial = crate::iso::fixed_from_iso(0, 12, 30); | ||
|
||
// Now we know that `iso_new::julian_from_fixed` is correct for jYMD(1, 1, 1): | ||
assert_eq!(new::julian_from_fixed(rata_die_initial), Ok((1, 1, 1))); | ||
|
||
let delta = 9_999; // 3_999_999; | ||
|
||
// This can be incorrect, but if it so, then on jYMD(1, 1, 1) we will figured that out, | ||
// because if it is incorrect we get incorrect value for `rata_die_i64` for the jYMD(1, 1, 1) | ||
// and `assert_eq!(iso_new::julian_from_fixed(rata_die), Ok((year, month, day)))` will panic | ||
let mut rata_die_i64 = new::fixed_from_julian(-delta, 1, 1).to_i64_date(); | ||
|
||
let mut year_day = 1; | ||
// Because rata die's stay each after each we can verify that in a range | ||
// all of `iso_new::julian_from_fixed` is correct, if that range contains the jYMD(1, 1, 1): | ||
for year in -delta..=delta { | ||
assert_year(year, |year, month, day| { | ||
if (month == 1) && (day == 1) { | ||
year_day = 1; | ||
} | ||
|
||
let rata_die = RataDie::new(rata_die_i64); | ||
assert_eq!(new::julian_from_fixed(rata_die), Ok((year, month, day))); | ||
assert_eq!(new::fixed_from_julian(year, month, day), rata_die); | ||
|
||
assert_eq!(new::day_of_year(year, month, day), year_day); | ||
|
||
if (month == 12) && (day == 31) { | ||
assert_eq!( | ||
year_day, | ||
365 + old::is_leap_year(year) as u16, | ||
"YMD: {year} {month} {day}" | ||
); | ||
} | ||
|
||
rata_die_i64 += 1; | ||
year_day += 1; | ||
}); | ||
} | ||
|
||
// FNS: {julian_from_fixed, fixed_from_julian, day_of_year}; | ||
// So now we sure that all fns from FNS is correct (at least in range -delta..=delta) | ||
} | ||
|
||
// [-] test correctness | ||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
|
||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
// [+] test the same result as prev algo | ||
|
||
#[test] | ||
fn day_of_year_the_same() { | ||
const N_YEAR: i32 = 9_999; | ||
const N_RANGE: RangeInclusive<i32> = (-N_YEAR)..=N_YEAR; | ||
|
||
let the_same_in_year = |year| { | ||
assert_year(year, |year, month, day| { | ||
assert_eq!( | ||
old::day_of_year(year, month, day), | ||
new::day_of_year(year, month, day), | ||
"YMD: {year} {month} {day}" | ||
); | ||
}) | ||
}; | ||
|
||
N_RANGE.for_each(the_same_in_year); | ||
MIN_YEAR_BOUND_RANGE.for_each(the_same_in_year); | ||
MAX_YEAR_BOUND_RANGE.for_each(the_same_in_year); | ||
} | ||
|
||
#[test] | ||
fn fixed_from_julian_the_same() { | ||
const N_YEAR: i32 = 9_999; | ||
const N_RANGE: RangeInclusive<i32> = (-N_YEAR)..=N_YEAR; | ||
|
||
let the_same_in_year = |year| { | ||
assert_year(year, |year, month, day| { | ||
assert_eq!( | ||
old::fixed_from_julian(year, month, day), | ||
new::fixed_from_julian(year, month, day), | ||
"YMD: {year} {month} {day}" | ||
); | ||
}) | ||
}; | ||
|
||
N_RANGE.for_each(the_same_in_year); | ||
MIN_YEAR_BOUND_RANGE.for_each(the_same_in_year); | ||
MAX_YEAR_BOUND_RANGE.for_each(the_same_in_year); | ||
} | ||
|
||
#[test] | ||
fn julian_from_fixed_the_same() { | ||
const N_YEAR: i32 = 9_999; | ||
const N_RANGE: RangeInclusive<i32> = (-N_YEAR)..=N_YEAR; | ||
|
||
let the_same_in_year = |year| { | ||
assert_year_rata_die(year, |year, month, day, date| { | ||
assert_eq!( | ||
old::julian_from_fixed(date), | ||
new::julian_from_fixed(date), | ||
"YMD: {year} {month} {day}" | ||
); | ||
}) | ||
}; | ||
|
||
N_RANGE.for_each(the_same_in_year); | ||
MAX_YEAR_BOUND_RANGE.for_each(the_same_in_year); | ||
|
||
// There was a mistake in the prev algo for the date (i32::MIN, 1, 1): | ||
((i32::MIN + 1)..=(i32::MIN + N_YEAR_BOUND)).for_each(the_same_in_year); | ||
let rata_die = new::fixed_from_julian(i32::MIN, 1, 1); | ||
let date = rata_die.to_i64_date(); | ||
for date in (date + 1)..=(date + 366) { | ||
let date = RataDie::new(date); | ||
assert_eq!(old::julian_from_fixed(date), new::julian_from_fixed(date),); | ||
} | ||
|
||
// The mistake: | ||
assert_eq!( | ||
old::julian_from_fixed(rata_die), | ||
Err(I32CastError::BelowMin) | ||
); | ||
assert_eq!(new::julian_from_fixed(rata_die), Ok((i32::MIN, 1, 1))); | ||
|
||
// Ok (should be BelowMin): | ||
assert_eq!( | ||
old::julian_from_fixed(RataDie::new(date - 1)), | ||
Err(I32CastError::BelowMin) | ||
); | ||
assert_eq!( | ||
new::julian_from_fixed(RataDie::new(date - 1)), | ||
Err(I32CastError::BelowMin) | ||
); | ||
} | ||
|
||
// [-] test the same result as prev algo | ||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
106 changes: 106 additions & 0 deletions
106
utils/calendrical_calculations/src/tests/julian_old_file.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
// This file is part of ICU4X. | ||
// | ||
// The contents of this file implement algorithms from Calendrical Calculations | ||
// by Reingold & Dershowitz, Cambridge University Press, 4th edition (2018), | ||
// which have been released as Lisp code at <https://github.com/EdReingold/calendar-code2/> | ||
// under the Apache-2.0 license. Accordingly, this file is released under | ||
// the Apache License, Version 2.0 which can be found at the calendrical_calculations | ||
// package root or at http://www.apache.org/licenses/LICENSE-2.0. | ||
|
||
use crate::helpers::{i64_to_i32, I32CastError}; | ||
use crate::rata_die::RataDie; | ||
|
||
// Julian epoch is equivalent to fixed_from_iso of December 30th of 0 year | ||
// 1st Jan of 1st year Julian is equivalent to December 30th of 0th year of ISO year | ||
const JULIAN_EPOCH: RataDie = RataDie::new(-1); | ||
|
||
/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/1ee51ecfaae6f856b0d7de3e36e9042100b4f424/calendar.l#L1684-L1687> | ||
#[inline(always)] | ||
pub const fn is_leap_year(year: i32) -> bool { | ||
year % 4 == 0 | ||
} | ||
|
||
/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/1ee51ecfaae6f856b0d7de3e36e9042100b4f424/calendar.l#L1689-L1709> | ||
pub const fn fixed_from_julian(year: i32, month: u8, day: u8) -> RataDie { | ||
let mut fixed = | ||
JULIAN_EPOCH.to_i64_date() - 1 + 365 * (year as i64 - 1) + (year as i64 - 1).div_euclid(4); | ||
debug_assert!(month > 0 && month < 13, "Month should be in range 1..=12."); | ||
fixed += day_of_year(year, month, day) as i64; | ||
RataDie::new(fixed) | ||
} | ||
|
||
/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/1ee51ecfaae6f856b0d7de3e36e9042100b4f424/calendar.l#L1711-L1738> | ||
pub fn julian_from_fixed(date: RataDie) -> Result<(i32, u8, u8), I32CastError> { | ||
// incorrect for rata_data(i32::MIN, 1, 1) | ||
let approx = (4 * date.to_i64_date() + 1464).div_euclid(1461); | ||
let year = i64_to_i32(approx)?; | ||
|
||
let prior_days = date | ||
- fixed_from_julian(year, 1, 1) | ||
- if is_leap_year(year) && date > fixed_from_julian(year, 2, 28) { | ||
1 | ||
} else { | ||
0 | ||
}; | ||
let adjusted_year = if prior_days >= 365 { | ||
year.saturating_add(1) | ||
} else { | ||
year | ||
}; | ||
let adjusted_prior_days = prior_days.rem_euclid(365); | ||
debug_assert!((0..365).contains(&adjusted_prior_days)); | ||
let month = if adjusted_prior_days < 31 { | ||
1 | ||
} else if adjusted_prior_days < 59 { | ||
2 | ||
} else if adjusted_prior_days < 90 { | ||
3 | ||
} else if adjusted_prior_days < 120 { | ||
4 | ||
} else if adjusted_prior_days < 151 { | ||
5 | ||
} else if adjusted_prior_days < 181 { | ||
6 | ||
} else if adjusted_prior_days < 212 { | ||
7 | ||
} else if adjusted_prior_days < 243 { | ||
8 | ||
} else if adjusted_prior_days < 273 { | ||
9 | ||
} else if adjusted_prior_days < 304 { | ||
10 | ||
} else if adjusted_prior_days < 334 { | ||
11 | ||
} else { | ||
12 | ||
}; | ||
let day = (date - fixed_from_julian(adjusted_year, month, 1) + 1) as u8; // as days_in_month is < u8::MAX | ||
debug_assert!(day <= 31, "Day assertion failed; date: {date:?}, adjusted_year: {adjusted_year}, prior_days: {prior_days}, month: {month}, day: {day}"); | ||
|
||
Ok((adjusted_year, month, day)) | ||
} | ||
|
||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
|
||
pub const fn day_of_year(year: i32, month: u8, day: u8) -> u16 { | ||
let mut day_before_month = match month { | ||
1 => 0, | ||
2 => 31, | ||
3 => 59, | ||
4 => 90, | ||
5 => 120, | ||
6 => 151, | ||
7 => 181, | ||
8 => 212, | ||
9 => 243, | ||
10 => 273, | ||
11 => 304, | ||
12 => 334, | ||
_ => 0, | ||
}; | ||
// Only add one if the month is after February (month > 2), since leap days are added to the end of February | ||
if month > 2 && is_leap_year(year) { | ||
day_before_month += 1; | ||
} | ||
day_before_month + day as u16 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
// We also can compile parts of this file for bench purpose (to cmp algo perfs) => `#[cfg(test)]` | ||
|
||
#[cfg(test)] | ||
mod iso; | ||
pub mod iso_old_algos; | ||
pub mod iso_old_file; | ||
|
||
#[cfg(test)] | ||
mod julian; | ||
pub mod julian_old_file; | ||
|
||
#[cfg(test)] | ||
pub mod helpful_consts { | ||
use core::ops::RangeInclusive; | ||
|
||
pub const N_YEAR_BOUND: i32 = 1234; // more than one cycle (400 years) | ||
pub const MIN_YEAR_BOUND_RANGE: RangeInclusive<i32> = i32::MIN..=(i32::MIN + N_YEAR_BOUND); | ||
pub const MAX_YEAR_BOUND_RANGE: RangeInclusive<i32> = (i32::MAX - N_YEAR_BOUND)..=i32::MAX; | ||
|
||
pub const MONTH_DAYS: [u8; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, the license was picked by a lawyer. Why would GNU be needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a realization of the algorithm(from the author of the article) in C/C++ and in the repo no apache 2.0 license
Here is a comment with mentioning it in the PR
So I don't sure is it necessary to add any of them or not
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah. We'd have to talk to the lawyer again for this.
Typically algorithms themselves aren't copyrightable, however we would indeed need to check with our lawyer to pull in this code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe the policy is, we can redistribute MIT (but not GPL) code under the Apache-2.0 license, and all third-party code should retain its copyright comments inline in the code, similar to the Reingold code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The first link goes to a file with the following comment at the top
According to the terms of the GPL license, which are fairly strict, an Apache-licensed crate such as
calendrical_calculations
would not be able to redistribute that code.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A workaround to this type of issue would be for any GPL-licensed code to live in its own crate, and then either
icu_calendar
orcalendrical_calculations
has an optional Cargo feature to consume it. Clients who would like the speedup and are okay consuming GPL-licensed code would need to manually enable the Cargo feature.I do not know whether such a GPL crate could live in this repository or whether it would need its own repository.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd rather avoid introducing GPL licensed code in our dep tree at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sffc @Manishearth
As I can understand you can implement algos from an article without such license restriction.
And if you will check the code by link and the code from PR it's will be pretty clear* that code was inspired by the article and only then it was matched with author's code for reference to authority. So maybe I can just remove link to the author implementation and leave only links to the article?
[*]: because of naming(unnamed const and very short names that say almost nothing (except for y/m/d ones, yeah they can say something, but nothing about how and why)) in the repo of the article's author. And in the PR even some consts was changed because of we have larger valid dates' interval -- in the author's code they are just magic numbers again; and how will you change such consts without understanding for what and why? And of course in the PR code there is plenty comments about why and how.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I understand that generally algorithms are not copyrightable, but either way, we will have to get approval from our lawyer for doing this, and they may choose to be more cautious about this. We were already quite cautious about the Reingold&Dershowitz algoritms.