#![forbid(unsafe_code)]
#![warn(missing_docs)]
mod dataset;
use crate::dataset::all_datasets;
use chrono::NaiveDate;
pub type PrefCode = u8;
#[derive(Debug, Clone)]
pub(crate) struct YearDataset {
pub year: u16,
pub effective_from: NaiveDate,
pub rates: Vec<(PrefCode, NaiveDate, u16)>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MinimumWageJpCompliance {
Compliant,
Short {
shortage_yen: u32,
required_yen: u32,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MinimumWageJpSuggestion {
pub year: u16,
pub effective_from: NaiveDate,
pub rate: u16,
}
#[derive(thiserror::Error, Debug, Eq, PartialEq)]
pub enum MinimumWageJpErr {
#[error("invalid pref code: {0}")]
InvalidPrefCode(PrefCode),
#[error("no dataset found for date {date}; prev={prev:?} next={next:?}")]
DatasetNotFoundForDate {
date: NaiveDate,
prev: Option<MinimumWageJpSuggestion>,
next: Option<MinimumWageJpSuggestion>,
},
#[error("no dataset found for year {year}")]
DatasetNotFoundForYear {
year: u16,
},
}
pub struct MinimumWageJp;
impl MinimumWageJp {
pub fn rate(pref: PrefCode) -> Result<u16, MinimumWageJpErr> {
Self::rate_on_date(today(), pref)
}
pub fn is_compliant(
pref: PrefCode,
hourly_yen: u32,
) -> Result<MinimumWageJpCompliance, MinimumWageJpErr> {
Self::is_compliant_on_date(today(), pref, hourly_yen)
}
pub fn rate_for_revision(year: u16, pref: PrefCode) -> Result<u16, MinimumWageJpErr> {
check_pref(pref)?;
let ds = all_datasets()
.iter()
.find(|d| d.year == year)
.ok_or(MinimumWageJpErr::DatasetNotFoundForYear { year })?;
Ok(pref_entry(ds, pref)?.2)
}
pub fn rate_on_date(date: NaiveDate, pref: PrefCode) -> Result<u16, MinimumWageJpErr> {
check_pref(pref)?;
let Some(ds) = dataset_prev_or_equal(date) else {
return Err(MinimumWageJpErr::DatasetNotFoundForDate {
date,
prev: None,
next: dataset_next_after(date).and_then(|n| suggestion_for(n, pref)),
});
};
let (_, eff, rate) = pref_entry(ds, pref)?;
if date >= eff {
return Ok(rate);
}
match dataset_prev_before(ds.effective_from) {
Some(prev) => Ok(pref_entry(prev, pref)?.2),
None => Err(MinimumWageJpErr::DatasetNotFoundForDate {
date,
prev: None,
next: Some(MinimumWageJpSuggestion {
year: ds.year,
effective_from: eff,
rate,
}),
}),
}
}
pub fn is_compliant_on_date(
date: NaiveDate,
pref: PrefCode,
hourly_yen: u32,
) -> Result<MinimumWageJpCompliance, MinimumWageJpErr> {
let required_yen = u32::from(Self::rate_on_date(date, pref)?);
if hourly_yen >= required_yen {
Ok(MinimumWageJpCompliance::Compliant)
} else {
Ok(MinimumWageJpCompliance::Short {
shortage_yen: required_yen - hourly_yen,
required_yen,
})
}
}
}
fn today() -> NaiveDate {
chrono::Local::now().date_naive()
}
fn check_pref(pref: PrefCode) -> Result<(), MinimumWageJpErr> {
if (1..=47).contains(&pref) {
Ok(())
} else {
Err(MinimumWageJpErr::InvalidPrefCode(pref))
}
}
fn pref_entry(
ds: &YearDataset,
pref: PrefCode,
) -> Result<(PrefCode, NaiveDate, u16), MinimumWageJpErr> {
ds.rates
.iter()
.find(|(p, _, _)| *p == pref)
.copied()
.ok_or(MinimumWageJpErr::InvalidPrefCode(pref))
}
fn suggestion_for(ds: &YearDataset, pref: PrefCode) -> Option<MinimumWageJpSuggestion> {
ds.rates
.iter()
.find(|(p, _, _)| *p == pref)
.map(|&(_, effective_from, rate)| MinimumWageJpSuggestion {
year: ds.year,
effective_from,
rate,
})
}
fn dataset_prev_or_equal(date: NaiveDate) -> Option<&'static YearDataset> {
all_datasets()
.iter()
.take_while(|ds| ds.effective_from <= date)
.last()
}
fn dataset_next_after(date: NaiveDate) -> Option<&'static YearDataset> {
all_datasets().iter().find(|ds| ds.effective_from > date)
}
fn dataset_prev_before(effective_from: NaiveDate) -> Option<&'static YearDataset> {
all_datasets()
.iter()
.take_while(|ds| ds.effective_from < effective_from)
.last()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dataset::nd;
#[test]
fn test_dataset_integrity() {
let datasets = all_datasets();
assert!(!datasets.is_empty());
for w in datasets.windows(2) {
assert!(w[0].year < w[1].year, "years must be ascending");
assert!(
w[0].effective_from < w[1].effective_from,
"effective_from must be ascending"
);
}
for ds in datasets {
let mut prefs: Vec<PrefCode> = ds.rates.iter().map(|(p, _, _)| *p).collect();
prefs.sort_unstable();
assert_eq!(
prefs,
(1..=47).collect::<Vec<PrefCode>>(),
"year {} must contain all 47 prefectures exactly once",
ds.year
);
let min_eff = ds.rates.iter().map(|(_, e, _)| *e).min().unwrap();
assert_eq!(
ds.effective_from, min_eff,
"year {}: effective_from must equal the earliest prefecture date",
ds.year
);
for (p, eff, rate) in &ds.rates {
assert!(
*rate > 0,
"year {} pref {}: rate must be positive",
ds.year,
p
);
assert!(
*eff >= ds.effective_from,
"year {} pref {}: date before dataset start",
ds.year,
p
);
}
}
}
#[test]
fn test_rate_for_revision() {
assert_eq!(MinimumWageJp::rate_for_revision(2024, 13).unwrap(), 1163);
assert_eq!(MinimumWageJp::rate_for_revision(2025, 1).unwrap(), 1075);
assert_eq!(MinimumWageJp::rate_for_revision(2025, 13).unwrap(), 1226);
assert_eq!(MinimumWageJp::rate_for_revision(2025, 47).unwrap(), 1023);
assert_eq!(
MinimumWageJp::rate_for_revision(2020, 1).unwrap_err(),
MinimumWageJpErr::DatasetNotFoundForYear { year: 2020 }
);
}
#[test]
fn test_rate_on_date() {
assert_eq!(
MinimumWageJp::rate_on_date(nd(2024, 10, 1), 13).unwrap(),
1163
);
assert_eq!(
MinimumWageJp::rate_on_date(nd(2025, 10, 2), 13).unwrap(),
1163
);
assert_eq!(
MinimumWageJp::rate_on_date(nd(2025, 10, 3), 13).unwrap(),
1226
);
assert_eq!(
MinimumWageJp::rate_on_date(nd(2025, 10, 2), 1).unwrap(),
1010
);
assert_eq!(
MinimumWageJp::rate_on_date(nd(2025, 10, 4), 1).unwrap(),
1075
);
assert_eq!(
MinimumWageJp::rate_on_date(nd(2026, 3, 30), 5).unwrap(),
951
);
assert_eq!(
MinimumWageJp::rate_on_date(nd(2026, 3, 31), 5).unwrap(),
1031
);
assert_eq!(
MinimumWageJp::rate_on_date(nd(2030, 1, 1), 13).unwrap(),
1226
);
}
#[test]
fn test_rate_on_date_before_first_dataset() {
assert_eq!(
MinimumWageJp::rate_on_date(nd(2024, 9, 30), 13).unwrap_err(),
MinimumWageJpErr::DatasetNotFoundForDate {
date: nd(2024, 9, 30),
prev: None,
next: Some(MinimumWageJpSuggestion {
year: 2024,
effective_from: nd(2024, 10, 1),
rate: 1163,
}),
}
);
}
#[test]
fn test_rate_on_date_before_pref_effective_without_prev() {
assert_eq!(
MinimumWageJp::rate_on_date(nd(2024, 10, 15), 3).unwrap_err(),
MinimumWageJpErr::DatasetNotFoundForDate {
date: nd(2024, 10, 15),
prev: None,
next: Some(MinimumWageJpSuggestion {
year: 2024,
effective_from: nd(2024, 10, 27),
rate: 952,
}),
}
);
assert_eq!(
MinimumWageJp::rate_on_date(nd(2024, 10, 27), 3).unwrap(),
952
);
}
#[test]
fn test_is_compliant_on_date() {
assert_eq!(
MinimumWageJp::is_compliant_on_date(nd(2025, 10, 3), 13, 1226).unwrap(),
MinimumWageJpCompliance::Compliant
);
assert_eq!(
MinimumWageJp::is_compliant_on_date(nd(2025, 10, 3), 13, 1225).unwrap(),
MinimumWageJpCompliance::Short {
shortage_yen: 1,
required_yen: 1226
}
);
assert_eq!(
MinimumWageJp::is_compliant_on_date(nd(2025, 10, 3), 1, 1010).unwrap(),
MinimumWageJpCompliance::Compliant
);
assert_eq!(
MinimumWageJp::is_compliant_on_date(nd(2025, 10, 4), 1, 1020).unwrap(),
MinimumWageJpCompliance::Short {
shortage_yen: 55,
required_yen: 1075
}
);
for pref in 1..=47 {
assert!(matches!(
MinimumWageJp::is_compliant_on_date(nd(2026, 4, 1), pref, 1000).unwrap(),
MinimumWageJpCompliance::Short { .. }
));
assert_eq!(
MinimumWageJp::is_compliant_on_date(nd(2026, 4, 1), pref, 3000).unwrap(),
MinimumWageJpCompliance::Compliant
);
}
}
#[test]
fn test_invalid_pref_code() {
for pref in [0, 48, 255] {
assert_eq!(
MinimumWageJp::rate_for_revision(2025, pref).unwrap_err(),
MinimumWageJpErr::InvalidPrefCode(pref)
);
assert_eq!(
MinimumWageJp::rate_on_date(nd(2025, 10, 1), pref).unwrap_err(),
MinimumWageJpErr::InvalidPrefCode(pref)
);
}
}
#[test]
fn test_today_apis() {
for pref in 1..=47 {
assert!(MinimumWageJp::rate(pref).is_ok());
assert!(MinimumWageJp::is_compliant(pref, 1000).is_ok());
}
}
#[test]
fn test_error_display() {
assert_eq!(
MinimumWageJpErr::InvalidPrefCode(48).to_string(),
"invalid pref code: 48"
);
assert_eq!(
MinimumWageJpErr::DatasetNotFoundForYear { year: 2020 }.to_string(),
"no dataset found for year 2020"
);
}
}