j-law-ruby 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +9 -0
- data/README.md +109 -0
- data/Rakefile +87 -0
- data/ext/j_law_ruby/extconf.rb +34 -0
- data/lib/j_law_ruby/build_support.rb +129 -0
- data/lib/j_law_ruby/c_ffi.rb +662 -0
- data/lib/j_law_ruby.rb +532 -0
- data/rake_support/vendor_rust.rb +51 -0
- data/test/test_c_ffi_adapter.rb +46 -0
- data/test/test_consumption_tax.rb +66 -0
- data/test/test_gemspec.rb +82 -0
- data/test/test_income_tax.rb +77 -0
- data/test/test_income_tax_deductions.rb +82 -0
- data/test/test_real_estate.rb +98 -0
- data/test/test_stamp_tax.rb +68 -0
- data/test/test_withholding_tax.rb +65 -0
- data/vendor/rust/Cargo.lock +235 -0
- data/vendor/rust/Cargo.toml +12 -0
- data/vendor/rust/crates/j-law-c-ffi/Cargo.toml +20 -0
- data/vendor/rust/crates/j-law-c-ffi/j_law_c_ffi.h +493 -0
- data/vendor/rust/crates/j-law-c-ffi/src/lib.rs +1553 -0
- data/vendor/rust/crates/j-law-core/Cargo.toml +18 -0
- data/vendor/rust/crates/j-law-core/src/domains/consumption_tax/calculator.rs +216 -0
- data/vendor/rust/crates/j-law-core/src/domains/consumption_tax/context.rs +29 -0
- data/vendor/rust/crates/j-law-core/src/domains/consumption_tax/mod.rs +9 -0
- data/vendor/rust/crates/j-law-core/src/domains/consumption_tax/params.rs +24 -0
- data/vendor/rust/crates/j-law-core/src/domains/consumption_tax/policy.rs +34 -0
- data/vendor/rust/crates/j-law-core/src/domains/income_tax/assessment.rs +76 -0
- data/vendor/rust/crates/j-law-core/src/domains/income_tax/calculator.rs +222 -0
- data/vendor/rust/crates/j-law-core/src/domains/income_tax/context.rs +79 -0
- data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/calculator.rs +167 -0
- data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/context.rs +172 -0
- data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/expense.rs +465 -0
- data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/mod.rs +20 -0
- data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/params.rs +205 -0
- data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/personal.rs +324 -0
- data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/types.rs +61 -0
- data/vendor/rust/crates/j-law-core/src/domains/income_tax/mod.rs +24 -0
- data/vendor/rust/crates/j-law-core/src/domains/income_tax/params.rs +109 -0
- data/vendor/rust/crates/j-law-core/src/domains/income_tax/policy.rs +103 -0
- data/vendor/rust/crates/j-law-core/src/domains/mod.rs +5 -0
- data/vendor/rust/crates/j-law-core/src/domains/real_estate/calculator.rs +197 -0
- data/vendor/rust/crates/j-law-core/src/domains/real_estate/context.rs +48 -0
- data/vendor/rust/crates/j-law-core/src/domains/real_estate/mod.rs +9 -0
- data/vendor/rust/crates/j-law-core/src/domains/real_estate/params.rs +43 -0
- data/vendor/rust/crates/j-law-core/src/domains/real_estate/policy.rs +40 -0
- data/vendor/rust/crates/j-law-core/src/domains/stamp_tax/calculator.rs +321 -0
- data/vendor/rust/crates/j-law-core/src/domains/stamp_tax/context.rs +408 -0
- data/vendor/rust/crates/j-law-core/src/domains/stamp_tax/mod.rs +12 -0
- data/vendor/rust/crates/j-law-core/src/domains/stamp_tax/params.rs +190 -0
- data/vendor/rust/crates/j-law-core/src/domains/stamp_tax/policy.rs +105 -0
- data/vendor/rust/crates/j-law-core/src/domains/withholding_tax/calculator.rs +247 -0
- data/vendor/rust/crates/j-law-core/src/domains/withholding_tax/context.rs +167 -0
- data/vendor/rust/crates/j-law-core/src/domains/withholding_tax/mod.rs +9 -0
- data/vendor/rust/crates/j-law-core/src/domains/withholding_tax/params.rs +80 -0
- data/vendor/rust/crates/j-law-core/src/domains/withholding_tax/policy.rs +49 -0
- data/vendor/rust/crates/j-law-core/src/error.rs +171 -0
- data/vendor/rust/crates/j-law-core/src/lib.rs +9 -0
- data/vendor/rust/crates/j-law-core/src/types/amount.rs +232 -0
- data/vendor/rust/crates/j-law-core/src/types/citation.rs +82 -0
- data/vendor/rust/crates/j-law-core/src/types/date.rs +280 -0
- data/vendor/rust/crates/j-law-core/src/types/mod.rs +11 -0
- data/vendor/rust/crates/j-law-core/src/types/rate.rs +219 -0
- data/vendor/rust/crates/j-law-core/src/types/rounding.rs +81 -0
- data/vendor/rust/crates/j-law-registry/Cargo.toml +15 -0
- data/vendor/rust/crates/j-law-registry/data/consumption_tax/consumption_tax.json +70 -0
- data/vendor/rust/crates/j-law-registry/data/income_tax/deductions.json +327 -0
- data/vendor/rust/crates/j-law-registry/data/income_tax/income_tax.json +352 -0
- data/vendor/rust/crates/j-law-registry/data/real_estate/brokerage_fee.json +125 -0
- data/vendor/rust/crates/j-law-registry/data/stamp_tax/stamp_tax.json +674 -0
- data/vendor/rust/crates/j-law-registry/data/withholding_tax/withholding_tax.json +70 -0
- data/vendor/rust/crates/j-law-registry/src/consumption_tax_loader.rs +325 -0
- data/vendor/rust/crates/j-law-registry/src/consumption_tax_schema.rs +49 -0
- data/vendor/rust/crates/j-law-registry/src/income_tax_deduction_loader.rs +636 -0
- data/vendor/rust/crates/j-law-registry/src/income_tax_deduction_schema.rs +111 -0
- data/vendor/rust/crates/j-law-registry/src/income_tax_loader.rs +445 -0
- data/vendor/rust/crates/j-law-registry/src/income_tax_schema.rs +44 -0
- data/vendor/rust/crates/j-law-registry/src/lib.rs +20 -0
- data/vendor/rust/crates/j-law-registry/src/loader.rs +221 -0
- data/vendor/rust/crates/j-law-registry/src/schema.rs +73 -0
- data/vendor/rust/crates/j-law-registry/src/stamp_tax_loader.rs +374 -0
- data/vendor/rust/crates/j-law-registry/src/stamp_tax_schema.rs +72 -0
- data/vendor/rust/crates/j-law-registry/src/validator.rs +204 -0
- data/vendor/rust/crates/j-law-registry/src/withholding_tax_loader.rs +310 -0
- data/vendor/rust/crates/j-law-registry/src/withholding_tax_schema.rs +61 -0
- metadata +148 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
use crate::InputError;
|
|
2
|
+
|
|
3
|
+
/// 法令の施行日・基準日を表す日付型。
|
|
4
|
+
///
|
|
5
|
+
/// 年月日の3要素で特定される暦日(西暦)。
|
|
6
|
+
/// 匿名タプル `(u16, u8, u8)` に代わる名前付き型。
|
|
7
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
8
|
+
pub struct LegalDate {
|
|
9
|
+
/// 年(西暦)
|
|
10
|
+
pub year: u16,
|
|
11
|
+
/// 月(1〜12)
|
|
12
|
+
pub month: u8,
|
|
13
|
+
/// 日(1〜31)
|
|
14
|
+
pub day: u8,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
impl LegalDate {
|
|
18
|
+
/// 年・月・日からインスタンスを作成する。
|
|
19
|
+
pub fn new(year: u16, month: u8, day: u8) -> Self {
|
|
20
|
+
Self { year, month, day }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/// 暦日として妥当な年月日かを検証する。
|
|
24
|
+
pub fn validate(&self) -> Result<(), InputError> {
|
|
25
|
+
let date = self.to_date_str();
|
|
26
|
+
|
|
27
|
+
if self.year == 0 {
|
|
28
|
+
return Err(InputError::InvalidDate {
|
|
29
|
+
date,
|
|
30
|
+
reason: "年は1以上を指定してください".into(),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if !(1..=12).contains(&self.month) {
|
|
35
|
+
return Err(InputError::InvalidDate {
|
|
36
|
+
date,
|
|
37
|
+
reason: "月は1〜12で指定してください".into(),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if self.day == 0 {
|
|
42
|
+
return Err(InputError::InvalidDate {
|
|
43
|
+
date,
|
|
44
|
+
reason: "日は1以上を指定してください".into(),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let max_day = Self::days_in_month(self.year, self.month);
|
|
49
|
+
if self.day > max_day {
|
|
50
|
+
return Err(InputError::InvalidDate {
|
|
51
|
+
date,
|
|
52
|
+
reason: format!("指定された月の日数を超えています: max_day={max_day}"),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
Ok(())
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// ISO 8601 形式("YYYY-MM-DD")の文字列に変換する。
|
|
60
|
+
///
|
|
61
|
+
/// Registry JSON の日付文字列との比較に使用する。
|
|
62
|
+
pub fn to_date_str(&self) -> String {
|
|
63
|
+
format!("{:04}-{:02}-{:02}", self.year, self.month, self.day)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// ISO 8601 形式("YYYY-MM-DD")の文字列からパースする。
|
|
67
|
+
///
|
|
68
|
+
/// 不正な形式の場合は `None` を返す。
|
|
69
|
+
pub fn from_date_str(s: &str) -> Option<Self> {
|
|
70
|
+
let bytes = s.as_bytes();
|
|
71
|
+
if bytes.len() != 10 || bytes[4] != b'-' || bytes[7] != b'-' {
|
|
72
|
+
return None;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let year: u16 = s[0..4].parse().ok()?;
|
|
76
|
+
let month: u8 = s[5..7].parse().ok()?;
|
|
77
|
+
let day: u8 = s[8..10].parse().ok()?;
|
|
78
|
+
if year == 0 || !(1..=12).contains(&month) {
|
|
79
|
+
return None;
|
|
80
|
+
}
|
|
81
|
+
if day < 1 || day > Self::days_in_month(year, month) {
|
|
82
|
+
return None;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
Some(Self { year, month, day })
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// 当該年が閏年かどうかを返す。
|
|
89
|
+
///
|
|
90
|
+
/// グレゴリオ暦の規則: 4で割り切れる && (100で割り切れない || 400で割り切れる)
|
|
91
|
+
fn is_leap_year(year: u16) -> bool {
|
|
92
|
+
let y = year as u32;
|
|
93
|
+
y.is_multiple_of(4) && (!y.is_multiple_of(100) || y.is_multiple_of(400))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// 指定した月の日数を返す。
|
|
97
|
+
fn days_in_month(year: u16, month: u8) -> u8 {
|
|
98
|
+
match month {
|
|
99
|
+
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
|
|
100
|
+
4 | 6 | 9 | 11 => 30,
|
|
101
|
+
2 => {
|
|
102
|
+
if Self::is_leap_year(year) {
|
|
103
|
+
29
|
|
104
|
+
} else {
|
|
105
|
+
28
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
_ => 0,
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/// 翌日の `LegalDate` を返す。
|
|
113
|
+
///
|
|
114
|
+
/// 月末・年末の繰り上がりを正確に処理する(chrono 不使用、純粋算術ベース)。
|
|
115
|
+
pub fn next_day(&self) -> Self {
|
|
116
|
+
let max_day = Self::days_in_month(self.year, self.month);
|
|
117
|
+
if self.day < max_day {
|
|
118
|
+
Self::new(self.year, self.month, self.day + 1)
|
|
119
|
+
} else if self.month < 12 {
|
|
120
|
+
Self::new(self.year, self.month + 1, 1)
|
|
121
|
+
} else {
|
|
122
|
+
Self::new(self.year + 1, 1, 1)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
impl From<(u16, u8, u8)> for LegalDate {
|
|
128
|
+
/// タプル `(year, month, day)` から `LegalDate` を構築する。
|
|
129
|
+
///
|
|
130
|
+
/// 既存コードとの互換性のために提供する。
|
|
131
|
+
fn from((year, month, day): (u16, u8, u8)) -> Self {
|
|
132
|
+
Self::new(year, month, day)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
#[cfg(test)]
|
|
137
|
+
mod tests {
|
|
138
|
+
use super::*;
|
|
139
|
+
|
|
140
|
+
#[test]
|
|
141
|
+
fn new_and_fields() {
|
|
142
|
+
let d = LegalDate::new(2024, 8, 1);
|
|
143
|
+
assert_eq!(d.year, 2024);
|
|
144
|
+
assert_eq!(d.month, 8);
|
|
145
|
+
assert_eq!(d.day, 1);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
#[test]
|
|
149
|
+
fn to_date_str_format() {
|
|
150
|
+
assert_eq!(LegalDate::new(2024, 8, 1).to_date_str(), "2024-08-01");
|
|
151
|
+
assert_eq!(LegalDate::new(2015, 1, 1).to_date_str(), "2015-01-01");
|
|
152
|
+
assert_eq!(LegalDate::new(2024, 12, 31).to_date_str(), "2024-12-31");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
#[test]
|
|
156
|
+
fn validate_accepts_leap_day() {
|
|
157
|
+
assert!(LegalDate::new(2024, 2, 29).validate().is_ok());
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
#[test]
|
|
161
|
+
fn validate_rejects_invalid_month() {
|
|
162
|
+
let result = LegalDate::new(2024, 13, 1).validate();
|
|
163
|
+
assert!(matches!(result, Err(InputError::InvalidDate { .. })));
|
|
164
|
+
let err_str = match result {
|
|
165
|
+
Err(err) => err.to_string(),
|
|
166
|
+
Ok(()) => String::new(),
|
|
167
|
+
};
|
|
168
|
+
assert!(err_str.contains("2024-13-01"));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
#[test]
|
|
172
|
+
fn validate_rejects_invalid_day() {
|
|
173
|
+
let result = LegalDate::new(2024, 2, 30).validate();
|
|
174
|
+
assert!(matches!(result, Err(InputError::InvalidDate { .. })));
|
|
175
|
+
let err_str = match result {
|
|
176
|
+
Err(err) => err.to_string(),
|
|
177
|
+
Ok(()) => String::new(),
|
|
178
|
+
};
|
|
179
|
+
assert!(err_str.contains("2024-02-30"));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
#[test]
|
|
183
|
+
fn validate_rejects_year_zero() {
|
|
184
|
+
let result = LegalDate::new(0, 1, 1).validate();
|
|
185
|
+
assert!(matches!(result, Err(InputError::InvalidDate { .. })));
|
|
186
|
+
let err_str = match result {
|
|
187
|
+
Err(err) => err.to_string(),
|
|
188
|
+
Ok(()) => String::new(),
|
|
189
|
+
};
|
|
190
|
+
assert!(err_str.contains("0000-01-01"));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
#[test]
|
|
194
|
+
fn from_date_str_valid() {
|
|
195
|
+
let d = LegalDate::from_date_str("2024-07-01").unwrap();
|
|
196
|
+
assert_eq!(d, LegalDate::new(2024, 7, 1));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
#[test]
|
|
200
|
+
fn from_date_str_invalid() {
|
|
201
|
+
assert!(LegalDate::from_date_str("2024-7-1").is_none());
|
|
202
|
+
assert!(LegalDate::from_date_str("20240701").is_none());
|
|
203
|
+
assert!(LegalDate::from_date_str("not-a-date").is_none());
|
|
204
|
+
assert!(LegalDate::from_date_str("0000-01-01").is_none());
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
#[test]
|
|
208
|
+
fn from_date_str_rejects_impossible_dates() {
|
|
209
|
+
assert!(LegalDate::from_date_str("2023-02-29").is_none());
|
|
210
|
+
assert!(LegalDate::from_date_str("2024-02-29").is_some());
|
|
211
|
+
assert!(LegalDate::from_date_str("2024-04-31").is_none());
|
|
212
|
+
assert!(LegalDate::from_date_str("2024-06-31").is_none());
|
|
213
|
+
assert!(LegalDate::from_date_str("2024-13-01").is_none());
|
|
214
|
+
assert!(LegalDate::from_date_str("2024-01-00").is_none());
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
#[test]
|
|
218
|
+
fn is_leap_year_cases() {
|
|
219
|
+
assert!(LegalDate::is_leap_year(2000));
|
|
220
|
+
assert!(!LegalDate::is_leap_year(1900));
|
|
221
|
+
assert!(LegalDate::is_leap_year(2024));
|
|
222
|
+
assert!(!LegalDate::is_leap_year(2023));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
#[test]
|
|
226
|
+
fn next_day_normal() {
|
|
227
|
+
assert_eq!(
|
|
228
|
+
LegalDate::new(2024, 7, 15).next_day(),
|
|
229
|
+
LegalDate::new(2024, 7, 16)
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
#[test]
|
|
234
|
+
fn next_day_month_end_30() {
|
|
235
|
+
assert_eq!(
|
|
236
|
+
LegalDate::new(2024, 6, 30).next_day(),
|
|
237
|
+
LegalDate::new(2024, 7, 1)
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
#[test]
|
|
242
|
+
fn next_day_month_end_31() {
|
|
243
|
+
assert_eq!(
|
|
244
|
+
LegalDate::new(2024, 7, 31).next_day(),
|
|
245
|
+
LegalDate::new(2024, 8, 1)
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
#[test]
|
|
250
|
+
fn next_day_year_end() {
|
|
251
|
+
assert_eq!(
|
|
252
|
+
LegalDate::new(2024, 12, 31).next_day(),
|
|
253
|
+
LegalDate::new(2025, 1, 1)
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
#[test]
|
|
258
|
+
fn next_day_feb_28_non_leap() {
|
|
259
|
+
assert_eq!(
|
|
260
|
+
LegalDate::new(2023, 2, 28).next_day(),
|
|
261
|
+
LegalDate::new(2023, 3, 1)
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
#[test]
|
|
266
|
+
fn next_day_feb_28_leap() {
|
|
267
|
+
assert_eq!(
|
|
268
|
+
LegalDate::new(2024, 2, 28).next_day(),
|
|
269
|
+
LegalDate::new(2024, 2, 29)
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
#[test]
|
|
274
|
+
fn next_day_feb_29_leap() {
|
|
275
|
+
assert_eq!(
|
|
276
|
+
LegalDate::new(2024, 2, 29).next_day(),
|
|
277
|
+
LegalDate::new(2024, 3, 1)
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
pub mod amount;
|
|
2
|
+
pub mod citation;
|
|
3
|
+
pub mod date;
|
|
4
|
+
pub mod rate;
|
|
5
|
+
pub mod rounding;
|
|
6
|
+
|
|
7
|
+
pub use amount::{FinalAmount, IntermediateAmount};
|
|
8
|
+
pub use citation::LegalCitation;
|
|
9
|
+
pub use date::LegalDate;
|
|
10
|
+
pub use rate::{MultiplyOrder, Rate};
|
|
11
|
+
pub use rounding::RoundingStrategy;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
use crate::error::{CalculationError, InputError, JLawError};
|
|
2
|
+
use crate::types::amount::IntermediateAmount;
|
|
3
|
+
use crate::types::rounding::RoundingStrategy;
|
|
4
|
+
|
|
5
|
+
/// 乗算順序の指定。
|
|
6
|
+
///
|
|
7
|
+
/// 端数処理が絡む計算では乗算と除算の順序で結果が変わる場合があるため、
|
|
8
|
+
/// 明示的に指定できるようにしている。
|
|
9
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
10
|
+
pub enum MultiplyOrder {
|
|
11
|
+
/// 先に分子を掛けてから分母で割る(精度優先)。
|
|
12
|
+
MultiplyFirst,
|
|
13
|
+
/// 先に分母で割ってから分子を掛ける(オーバーフロー回避優先)。
|
|
14
|
+
DivideFirst,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/// 分数で表された比率(例: 5/100 = 5%)。
|
|
18
|
+
///
|
|
19
|
+
/// float を使わず整数分数で保持することで、法令計算に必要な
|
|
20
|
+
/// 再現性のある端数処理を保証する。
|
|
21
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
22
|
+
pub struct Rate {
|
|
23
|
+
/// 分子。
|
|
24
|
+
pub(crate) numer: u64,
|
|
25
|
+
/// 分母。0 は不正値。
|
|
26
|
+
pub(crate) denom: u64,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
impl Rate {
|
|
30
|
+
/// 分数比率を作る。`denom == 0` の場合はエラーを返す。
|
|
31
|
+
pub fn new(numer: u64, denom: u64) -> Result<Self, InputError> {
|
|
32
|
+
if denom == 0 {
|
|
33
|
+
return Err(InputError::ZeroDenominator);
|
|
34
|
+
}
|
|
35
|
+
Ok(Self { numer, denom })
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// 分子を返す。
|
|
39
|
+
pub fn numer(&self) -> u64 {
|
|
40
|
+
self.numer
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// 分母を返す。
|
|
44
|
+
pub fn denom(&self) -> u64 {
|
|
45
|
+
self.denom
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// `amount` にこのレートを適用して新しい `IntermediateAmount` を返す。
|
|
49
|
+
///
|
|
50
|
+
/// `amount` の整数部(`whole`)のみに適用し、端数部(`numer`/`denom`)は無視する。
|
|
51
|
+
///
|
|
52
|
+
/// # Important
|
|
53
|
+
/// `amount.numer` / `amount.denom` が非ゼロ(端数あり)の場合、
|
|
54
|
+
/// その端数部分は計算に含まれず**黙って切り捨てられます**。
|
|
55
|
+
/// 端数を保持した状態でレートを適用したい場合は、
|
|
56
|
+
/// 事前に `amount.finalize(rounding)` を呼び出して整数化してください。
|
|
57
|
+
///
|
|
58
|
+
/// # エラー
|
|
59
|
+
/// - `self.denom == 0` の場合は `InputError::ZeroDenominator` を返す。
|
|
60
|
+
/// - `MultiplyOrder::MultiplyFirst` で `base * self.numer` がオーバーフローした場合は
|
|
61
|
+
/// `CalculationError::Overflow` を返す。
|
|
62
|
+
pub fn apply(
|
|
63
|
+
&self,
|
|
64
|
+
amount: &IntermediateAmount,
|
|
65
|
+
order: MultiplyOrder,
|
|
66
|
+
rounding: RoundingStrategy,
|
|
67
|
+
) -> Result<IntermediateAmount, JLawError> {
|
|
68
|
+
if self.denom == 0 {
|
|
69
|
+
return Err(InputError::ZeroDenominator.into());
|
|
70
|
+
}
|
|
71
|
+
let base = amount.whole;
|
|
72
|
+
let result_whole = match order {
|
|
73
|
+
MultiplyOrder::MultiplyFirst => {
|
|
74
|
+
let product =
|
|
75
|
+
base.checked_mul(self.numer)
|
|
76
|
+
.ok_or_else(|| CalculationError::Overflow {
|
|
77
|
+
step: format!("rate_apply: {} * {}", base, self.numer),
|
|
78
|
+
})?;
|
|
79
|
+
rounding.apply_ratio(product, self.denom)?
|
|
80
|
+
}
|
|
81
|
+
MultiplyOrder::DivideFirst => {
|
|
82
|
+
rounding.apply_ratio(base / self.denom * self.numer, 1)?
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
Ok(IntermediateAmount::from_exact(result_whole))
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#[cfg(test)]
|
|
90
|
+
#[allow(clippy::disallowed_methods)]
|
|
91
|
+
mod tests {
|
|
92
|
+
use super::*;
|
|
93
|
+
|
|
94
|
+
fn exact(yen: u64) -> IntermediateAmount {
|
|
95
|
+
IntermediateAmount::from_exact(yen)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
#[test]
|
|
99
|
+
fn rate_5_percent_multiply_first() {
|
|
100
|
+
// 2_000_000 × 5/100 = 100_000
|
|
101
|
+
let rate = Rate::new(5, 100).unwrap();
|
|
102
|
+
let result = rate
|
|
103
|
+
.apply(
|
|
104
|
+
&exact(2_000_000),
|
|
105
|
+
MultiplyOrder::MultiplyFirst,
|
|
106
|
+
RoundingStrategy::Floor,
|
|
107
|
+
)
|
|
108
|
+
.unwrap();
|
|
109
|
+
assert_eq!(
|
|
110
|
+
result.finalize(RoundingStrategy::Floor).unwrap().as_yen(),
|
|
111
|
+
100_000
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
#[test]
|
|
116
|
+
fn rate_4_percent_tier2() {
|
|
117
|
+
// (4_000_000 - 2_000_000) × 4/100 = 80_000
|
|
118
|
+
let rate = Rate::new(4, 100).unwrap();
|
|
119
|
+
let result = rate
|
|
120
|
+
.apply(
|
|
121
|
+
&exact(2_000_000),
|
|
122
|
+
MultiplyOrder::MultiplyFirst,
|
|
123
|
+
RoundingStrategy::Floor,
|
|
124
|
+
)
|
|
125
|
+
.unwrap();
|
|
126
|
+
assert_eq!(
|
|
127
|
+
result.finalize(RoundingStrategy::Floor).unwrap().as_yen(),
|
|
128
|
+
80_000
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
#[test]
|
|
133
|
+
fn rate_3_percent_tier3() {
|
|
134
|
+
// (5_000_000 - 4_000_000) × 3/100 = 30_000
|
|
135
|
+
let rate = Rate::new(3, 100).unwrap();
|
|
136
|
+
let result = rate
|
|
137
|
+
.apply(
|
|
138
|
+
&exact(1_000_000),
|
|
139
|
+
MultiplyOrder::MultiplyFirst,
|
|
140
|
+
RoundingStrategy::Floor,
|
|
141
|
+
)
|
|
142
|
+
.unwrap();
|
|
143
|
+
assert_eq!(
|
|
144
|
+
result.finalize(RoundingStrategy::Floor).unwrap().as_yen(),
|
|
145
|
+
30_000
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
#[test]
|
|
150
|
+
fn multiply_first_vs_divide_first_differ() {
|
|
151
|
+
// 10 × 1/3:
|
|
152
|
+
// MultiplyFirst: floor(10/3) = 3
|
|
153
|
+
// DivideFirst: floor(10/3) * 1 = 3 ← 同じ
|
|
154
|
+
// 差が出るケース: 7 × 3/4
|
|
155
|
+
// MultiplyFirst: floor(21/4) = 5
|
|
156
|
+
// DivideFirst: floor(7/4) * 3 = 1 * 3 = 3
|
|
157
|
+
let rate = Rate::new(3, 4).unwrap();
|
|
158
|
+
let mf = rate
|
|
159
|
+
.apply(
|
|
160
|
+
&exact(7),
|
|
161
|
+
MultiplyOrder::MultiplyFirst,
|
|
162
|
+
RoundingStrategy::Floor,
|
|
163
|
+
)
|
|
164
|
+
.unwrap();
|
|
165
|
+
let df = rate
|
|
166
|
+
.apply(
|
|
167
|
+
&exact(7),
|
|
168
|
+
MultiplyOrder::DivideFirst,
|
|
169
|
+
RoundingStrategy::Floor,
|
|
170
|
+
)
|
|
171
|
+
.unwrap();
|
|
172
|
+
assert_eq!(mf.finalize(RoundingStrategy::Floor).unwrap().as_yen(), 5);
|
|
173
|
+
assert_eq!(df.finalize(RoundingStrategy::Floor).unwrap().as_yen(), 3);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
#[test]
|
|
177
|
+
fn tax_10_percent() {
|
|
178
|
+
// 210_000 × 10/100 = 21_000
|
|
179
|
+
let rate = Rate::new(10, 100).unwrap();
|
|
180
|
+
let result = rate
|
|
181
|
+
.apply(
|
|
182
|
+
&exact(210_000),
|
|
183
|
+
MultiplyOrder::MultiplyFirst,
|
|
184
|
+
RoundingStrategy::Floor,
|
|
185
|
+
)
|
|
186
|
+
.unwrap();
|
|
187
|
+
assert_eq!(
|
|
188
|
+
result.finalize(RoundingStrategy::Floor).unwrap().as_yen(),
|
|
189
|
+
21_000
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
#[test]
|
|
194
|
+
fn zero_denominator_returns_error() {
|
|
195
|
+
// Rate::new(1, 0) はエラーを返す
|
|
196
|
+
let result = Rate::new(1, 0);
|
|
197
|
+
assert!(result.is_err());
|
|
198
|
+
assert!(matches!(
|
|
199
|
+
result.unwrap_err(),
|
|
200
|
+
crate::error::InputError::ZeroDenominator
|
|
201
|
+
));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
#[test]
|
|
205
|
+
fn overflow_returns_calculation_error() {
|
|
206
|
+
// u64::MAX * 2 はオーバーフローする
|
|
207
|
+
let rate = Rate::new(2, 1).unwrap();
|
|
208
|
+
let result = rate.apply(
|
|
209
|
+
&exact(u64::MAX),
|
|
210
|
+
MultiplyOrder::MultiplyFirst,
|
|
211
|
+
RoundingStrategy::Floor,
|
|
212
|
+
);
|
|
213
|
+
assert!(result.is_err());
|
|
214
|
+
assert!(matches!(
|
|
215
|
+
result.unwrap_err(),
|
|
216
|
+
crate::error::JLawError::Calculation(crate::error::CalculationError::Overflow { .. })
|
|
217
|
+
));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/// 端数処理戦略。
|
|
2
|
+
///
|
|
3
|
+
/// 法令計算では端数処理の根拠を明示する必要があるため、
|
|
4
|
+
/// `f64::floor` や `f64::round` を使わず、この型で整数演算する。
|
|
5
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
6
|
+
pub enum RoundingStrategy {
|
|
7
|
+
/// 切り捨て(床関数)。法令計算で最も多く使われる。
|
|
8
|
+
Floor,
|
|
9
|
+
/// 四捨五入。
|
|
10
|
+
HalfUp,
|
|
11
|
+
/// 切り上げ(天井関数)。
|
|
12
|
+
Ceil,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
impl RoundingStrategy {
|
|
16
|
+
/// `numer / denom` を整数で丸める(整数演算のみ・float不使用)。
|
|
17
|
+
///
|
|
18
|
+
/// # エラー
|
|
19
|
+
/// `denom == 0` の場合は `InputError::ZeroDenominator` を返す。
|
|
20
|
+
pub(crate) fn apply_ratio(
|
|
21
|
+
self,
|
|
22
|
+
numer: u64,
|
|
23
|
+
denom: u64,
|
|
24
|
+
) -> Result<u64, crate::error::InputError> {
|
|
25
|
+
if denom == 0 {
|
|
26
|
+
return Err(crate::error::InputError::ZeroDenominator);
|
|
27
|
+
}
|
|
28
|
+
Ok(match self {
|
|
29
|
+
RoundingStrategy::Floor => numer / denom,
|
|
30
|
+
RoundingStrategy::Ceil => numer.div_ceil(denom),
|
|
31
|
+
RoundingStrategy::HalfUp => {
|
|
32
|
+
// numer / denom を四捨五入: (numer * 2 + denom) / (denom * 2)
|
|
33
|
+
// オーバーフロー対策: numer + denom/2 が安全な範囲かチェック不要
|
|
34
|
+
// (denom は通常小さい値のため問題ない)
|
|
35
|
+
(numer + denom / 2) / denom
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#[cfg(test)]
|
|
42
|
+
mod tests {
|
|
43
|
+
#![allow(clippy::disallowed_methods)] // テストコードでは unwrap() 使用可能
|
|
44
|
+
use super::*;
|
|
45
|
+
|
|
46
|
+
#[test]
|
|
47
|
+
fn floor_truncates() {
|
|
48
|
+
assert_eq!(RoundingStrategy::Floor.apply_ratio(10, 3).unwrap(), 3);
|
|
49
|
+
assert_eq!(RoundingStrategy::Floor.apply_ratio(9, 3).unwrap(), 3);
|
|
50
|
+
assert_eq!(RoundingStrategy::Floor.apply_ratio(0, 5).unwrap(), 0);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#[test]
|
|
54
|
+
fn ceil_rounds_up() {
|
|
55
|
+
assert_eq!(RoundingStrategy::Ceil.apply_ratio(10, 3).unwrap(), 4);
|
|
56
|
+
assert_eq!(RoundingStrategy::Ceil.apply_ratio(9, 3).unwrap(), 3);
|
|
57
|
+
assert_eq!(RoundingStrategy::Ceil.apply_ratio(1, 5).unwrap(), 1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#[test]
|
|
61
|
+
fn half_up_rounds() {
|
|
62
|
+
// 5 / 2 = 2.5 → 3
|
|
63
|
+
assert_eq!(RoundingStrategy::HalfUp.apply_ratio(5, 2).unwrap(), 3);
|
|
64
|
+
// 4 / 2 = 2.0 → 2
|
|
65
|
+
assert_eq!(RoundingStrategy::HalfUp.apply_ratio(4, 2).unwrap(), 2);
|
|
66
|
+
// 7 / 3 = 2.333... → 2
|
|
67
|
+
assert_eq!(RoundingStrategy::HalfUp.apply_ratio(7, 3).unwrap(), 2);
|
|
68
|
+
// 8 / 3 = 2.666... → 3
|
|
69
|
+
assert_eq!(RoundingStrategy::HalfUp.apply_ratio(8, 3).unwrap(), 3);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#[test]
|
|
73
|
+
fn zero_denominator_returns_error() {
|
|
74
|
+
let result = RoundingStrategy::Floor.apply_ratio(10, 0);
|
|
75
|
+
assert!(result.is_err());
|
|
76
|
+
assert!(matches!(
|
|
77
|
+
result.unwrap_err(),
|
|
78
|
+
crate::error::InputError::ZeroDenominator
|
|
79
|
+
));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "j-law-registry"
|
|
3
|
+
version = "0.0.3"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
rust-version = "1.94"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
publish = false
|
|
8
|
+
|
|
9
|
+
[lints]
|
|
10
|
+
workspace = true
|
|
11
|
+
|
|
12
|
+
[dependencies]
|
|
13
|
+
j-law-core = { path = "../j-law-core" }
|
|
14
|
+
serde = { version = "1", features = ["derive"] }
|
|
15
|
+
serde_json = "1"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"domain": "consumption_tax",
|
|
3
|
+
"description": "消費税法第29条に基づく消費税率の歴史",
|
|
4
|
+
"history": [
|
|
5
|
+
{
|
|
6
|
+
"effective_from": "1989-04-01",
|
|
7
|
+
"effective_until": "1997-03-31",
|
|
8
|
+
"status": "superseded",
|
|
9
|
+
"citation": {
|
|
10
|
+
"law_id": "shohizei-29",
|
|
11
|
+
"law_name": "消費税法",
|
|
12
|
+
"article": 29,
|
|
13
|
+
"paragraph": null,
|
|
14
|
+
"ministry": "財務省"
|
|
15
|
+
},
|
|
16
|
+
"params": {
|
|
17
|
+
"standard_rate": { "numer": 3, "denom": 100 },
|
|
18
|
+
"reduced_rate": null
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"effective_from": "1997-04-01",
|
|
23
|
+
"effective_until": "2014-03-31",
|
|
24
|
+
"status": "superseded",
|
|
25
|
+
"citation": {
|
|
26
|
+
"law_id": "shohizei-29",
|
|
27
|
+
"law_name": "消費税法",
|
|
28
|
+
"article": 29,
|
|
29
|
+
"paragraph": null,
|
|
30
|
+
"ministry": "財務省"
|
|
31
|
+
},
|
|
32
|
+
"params": {
|
|
33
|
+
"standard_rate": { "numer": 5, "denom": 100 },
|
|
34
|
+
"reduced_rate": null
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"effective_from": "2014-04-01",
|
|
39
|
+
"effective_until": "2019-09-30",
|
|
40
|
+
"status": "superseded",
|
|
41
|
+
"citation": {
|
|
42
|
+
"law_id": "shohizei-29",
|
|
43
|
+
"law_name": "消費税法",
|
|
44
|
+
"article": 29,
|
|
45
|
+
"paragraph": null,
|
|
46
|
+
"ministry": "財務省"
|
|
47
|
+
},
|
|
48
|
+
"params": {
|
|
49
|
+
"standard_rate": { "numer": 8, "denom": 100 },
|
|
50
|
+
"reduced_rate": null
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"effective_from": "2019-10-01",
|
|
55
|
+
"effective_until": null,
|
|
56
|
+
"status": "active",
|
|
57
|
+
"citation": {
|
|
58
|
+
"law_id": "shohizei-29",
|
|
59
|
+
"law_name": "消費税法",
|
|
60
|
+
"article": 29,
|
|
61
|
+
"paragraph": null,
|
|
62
|
+
"ministry": "財務省"
|
|
63
|
+
},
|
|
64
|
+
"params": {
|
|
65
|
+
"standard_rate": { "numer": 10, "denom": 100 },
|
|
66
|
+
"reduced_rate": { "numer": 8, "denom": 100 }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
}
|