minimum_wage_jp 1.0.1

Japan minimum wage by prefecture: get rate for a date and check compliance
Documentation
// src/lib.rs
#![forbid(unsafe_code)]

use crate::dataset::all_datasets;
use chrono::NaiveDate;

type PrefCode = u8; // 1..=47

#[derive(Debug, Clone)]
pub(crate) struct YearDataset {
    pub year: u16,
    pub effective_from: NaiveDate,              // Ymd (yyyymmdd)
    pub rates: Vec<(PrefCode, NaiveDate, u16)>, // (PrefCode, Ymd, rate)
}

#[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 },
}

mod dataset;

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 })?;
        let rate = ds
            .rates
            .iter()
            .find(|(p, _, _)| *p == pref)
            .map(|(_, _, r)| *r)
            .ok_or(MinimumWageJpErr::InvalidPrefCode(pref))?;
        Ok(rate)
    }

    /// 指定日付における地域別最低賃金(円)
    pub fn rate_on_date(date: NaiveDate, pref: PrefCode) -> Result<u16, MinimumWageJpErr> {
        check_pref(pref)?;
        let ds = match dataset_prev_or_equal(date) {
            Some(d) => d,
            None => {
                // 前が無い → 次の最初の候補だけ提示
                let next = dataset_next_after(date).map(|n| {
                    let rate = n
                        .rates
                        .iter()
                        .find(|(p, _, _)| *p == pref)
                        .map(|(_, _, r)| *r)
                        .unwrap_or(0);
                    MinimumWageJpSuggestion {
                        year: n.year,
                        effective_from: n.effective_from,
                        rate,
                    }
                });
                return Err(MinimumWageJpErr::DatasetNotFoundForDate {
                    date,
                    prev: None,
                    next,
                });
            }
        };

        // 当該県の適用開始が遅い場合は前年版に戻す
        let (_, eff, rate) = ds
            .rates
            .iter()
            .find(|(p, _, _)| *p == pref)
            .copied()
            .ok_or(MinimumWageJpErr::InvalidPrefCode(pref))?;
        if date < eff {
            // 直前の年版
            if let Some(prev) = dataset_prev_before(ds.effective_from) {
                let rate_prev = prev
                    .rates
                    .iter()
                    .find(|(p, _, _)| *p == pref)
                    .map(|(_, _, r)| *r)
                    .ok_or(MinimumWageJpErr::InvalidPrefCode(pref))?;
                return Ok(rate_prev);
            }
        }

        Ok(rate)
    }

    /// 指定時給が最低賃金を満たすか(不足額つき)
    pub fn is_compliant_on_date(
        date: NaiveDate,
        pref: PrefCode,
        hourly_yen: u32,
    ) -> Result<MinimumWageJpCompliance, MinimumWageJpErr> {
        match Self::rate_on_date(date, pref) {
            Ok(rate) => {
                let rate_u32 = rate as u32;
                if hourly_yen >= rate_u32 {
                    Ok(MinimumWageJpCompliance::Compliant)
                } else {
                    Ok(MinimumWageJpCompliance::Short {
                        shortage_yen: rate_u32 - hourly_yen,
                        required_yen: rate_u32,
                    })
                }
            }
            Err(e) => Err(e),
        }
    }
}

/// ローカル日付(NaiveDate)を返す
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 dataset_prev_or_equal(date: NaiveDate) -> Option<&'static YearDataset> {
    let mut sel: Option<&YearDataset> = None;
    for ds in all_datasets() {
        if ds.effective_from <= date {
            sel = Some(ds);
        } else {
            break;
        }
    }
    sel
}

fn dataset_next_after(date: NaiveDate) -> Option<&'static YearDataset> {
    for ds in all_datasets() {
        if ds.effective_from > date {
            return Some(ds);
        }
    }
    None
}

/// 指定された effective_from 日付より前の最新の年版データセットを探す関数
fn dataset_prev_before(effective_from: NaiveDate) -> Option<&'static YearDataset> {
    let mut latest: Option<&YearDataset> = None;
    for ds in all_datasets() {
        if ds.effective_from < effective_from {
            latest = Some(ds);
        } else {
            break;
        }
    }
    latest
}
#[cfg(test)]
mod tests {
    use super::*;
    use crate::dataset::nd;

    #[test]
    fn test_rate_for_revision() {
        assert_eq!(
            MinimumWageJp::rate_for_revision(2020, 1).unwrap_err(),
            MinimumWageJpErr::DatasetNotFoundForYear { year: 2020 }
        );
        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);
    }

    #[test]
    fn test_rate_at() {
        assert!(MinimumWageJp::rate_on_date(nd(2024, 1, 1), 1).is_err(),);
        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(2025, 10, 1), 13).unwrap(),
            1226
        );
    }

    #[test]
    fn test_is_compliant() {
        for i in 1..=47 {
            match MinimumWageJp::is_compliant_on_date(nd(2025, 10, 1), i, 1000).unwrap() {
                MinimumWageJpCompliance::Compliant => {
                    assert!(false, "should not be compliant: {}", i)
                }
                MinimumWageJpCompliance::Short { .. } => {
                    assert!(true)
                }
            }
        }
        for i in 1..=47 {
            match MinimumWageJp::is_compliant_on_date(nd(2030, 10, 1), i, 3000).unwrap() {
                MinimumWageJpCompliance::Compliant => {
                    assert!(true)
                }
                MinimumWageJpCompliance::Short { .. } => {
                    assert!(false, "should not be compliant: {}", i)
                }
            }
        }
        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
            }
        );
        assert_eq!(
            MinimumWageJp::is_compliant_on_date(nd(2025, 10, 4), 1, 1075).unwrap(),
            MinimumWageJpCompliance::Compliant
        );
        assert_eq!(
            MinimumWageJp::is_compliant_on_date(nd(2025, 10, 4), 1, 1074).unwrap(),
            MinimumWageJpCompliance::Short {
                shortage_yen: 1,
                required_yen: 1075
            }
        );
        assert_eq!(
            MinimumWageJp::is_compliant_on_date(nd(2025, 10, 4), 1, 1000).unwrap(),
            MinimumWageJpCompliance::Short {
                shortage_yen: 75,
                required_yen: 1075
            }
        );
    }

    #[test]
    fn test_errors() {
        assert!(matches!(
            MinimumWageJp::rate_for_revision(2025, 0),
            Err(MinimumWageJpErr::InvalidPrefCode(0))
        ));
        assert!(matches!(
            MinimumWageJp::rate_for_revision(2025, 48),
            Err(MinimumWageJpErr::InvalidPrefCode(48))
        ));
        assert!(matches!(
            MinimumWageJp::rate_for_revision(2023, 1),
            Err(MinimumWageJpErr::DatasetNotFoundForYear { year: 2023 })
        ));
    }
}