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.
Files changed (87) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +9 -0
  3. data/README.md +109 -0
  4. data/Rakefile +87 -0
  5. data/ext/j_law_ruby/extconf.rb +34 -0
  6. data/lib/j_law_ruby/build_support.rb +129 -0
  7. data/lib/j_law_ruby/c_ffi.rb +662 -0
  8. data/lib/j_law_ruby.rb +532 -0
  9. data/rake_support/vendor_rust.rb +51 -0
  10. data/test/test_c_ffi_adapter.rb +46 -0
  11. data/test/test_consumption_tax.rb +66 -0
  12. data/test/test_gemspec.rb +82 -0
  13. data/test/test_income_tax.rb +77 -0
  14. data/test/test_income_tax_deductions.rb +82 -0
  15. data/test/test_real_estate.rb +98 -0
  16. data/test/test_stamp_tax.rb +68 -0
  17. data/test/test_withholding_tax.rb +65 -0
  18. data/vendor/rust/Cargo.lock +235 -0
  19. data/vendor/rust/Cargo.toml +12 -0
  20. data/vendor/rust/crates/j-law-c-ffi/Cargo.toml +20 -0
  21. data/vendor/rust/crates/j-law-c-ffi/j_law_c_ffi.h +493 -0
  22. data/vendor/rust/crates/j-law-c-ffi/src/lib.rs +1553 -0
  23. data/vendor/rust/crates/j-law-core/Cargo.toml +18 -0
  24. data/vendor/rust/crates/j-law-core/src/domains/consumption_tax/calculator.rs +216 -0
  25. data/vendor/rust/crates/j-law-core/src/domains/consumption_tax/context.rs +29 -0
  26. data/vendor/rust/crates/j-law-core/src/domains/consumption_tax/mod.rs +9 -0
  27. data/vendor/rust/crates/j-law-core/src/domains/consumption_tax/params.rs +24 -0
  28. data/vendor/rust/crates/j-law-core/src/domains/consumption_tax/policy.rs +34 -0
  29. data/vendor/rust/crates/j-law-core/src/domains/income_tax/assessment.rs +76 -0
  30. data/vendor/rust/crates/j-law-core/src/domains/income_tax/calculator.rs +222 -0
  31. data/vendor/rust/crates/j-law-core/src/domains/income_tax/context.rs +79 -0
  32. data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/calculator.rs +167 -0
  33. data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/context.rs +172 -0
  34. data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/expense.rs +465 -0
  35. data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/mod.rs +20 -0
  36. data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/params.rs +205 -0
  37. data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/personal.rs +324 -0
  38. data/vendor/rust/crates/j-law-core/src/domains/income_tax/deduction/types.rs +61 -0
  39. data/vendor/rust/crates/j-law-core/src/domains/income_tax/mod.rs +24 -0
  40. data/vendor/rust/crates/j-law-core/src/domains/income_tax/params.rs +109 -0
  41. data/vendor/rust/crates/j-law-core/src/domains/income_tax/policy.rs +103 -0
  42. data/vendor/rust/crates/j-law-core/src/domains/mod.rs +5 -0
  43. data/vendor/rust/crates/j-law-core/src/domains/real_estate/calculator.rs +197 -0
  44. data/vendor/rust/crates/j-law-core/src/domains/real_estate/context.rs +48 -0
  45. data/vendor/rust/crates/j-law-core/src/domains/real_estate/mod.rs +9 -0
  46. data/vendor/rust/crates/j-law-core/src/domains/real_estate/params.rs +43 -0
  47. data/vendor/rust/crates/j-law-core/src/domains/real_estate/policy.rs +40 -0
  48. data/vendor/rust/crates/j-law-core/src/domains/stamp_tax/calculator.rs +321 -0
  49. data/vendor/rust/crates/j-law-core/src/domains/stamp_tax/context.rs +408 -0
  50. data/vendor/rust/crates/j-law-core/src/domains/stamp_tax/mod.rs +12 -0
  51. data/vendor/rust/crates/j-law-core/src/domains/stamp_tax/params.rs +190 -0
  52. data/vendor/rust/crates/j-law-core/src/domains/stamp_tax/policy.rs +105 -0
  53. data/vendor/rust/crates/j-law-core/src/domains/withholding_tax/calculator.rs +247 -0
  54. data/vendor/rust/crates/j-law-core/src/domains/withholding_tax/context.rs +167 -0
  55. data/vendor/rust/crates/j-law-core/src/domains/withholding_tax/mod.rs +9 -0
  56. data/vendor/rust/crates/j-law-core/src/domains/withholding_tax/params.rs +80 -0
  57. data/vendor/rust/crates/j-law-core/src/domains/withholding_tax/policy.rs +49 -0
  58. data/vendor/rust/crates/j-law-core/src/error.rs +171 -0
  59. data/vendor/rust/crates/j-law-core/src/lib.rs +9 -0
  60. data/vendor/rust/crates/j-law-core/src/types/amount.rs +232 -0
  61. data/vendor/rust/crates/j-law-core/src/types/citation.rs +82 -0
  62. data/vendor/rust/crates/j-law-core/src/types/date.rs +280 -0
  63. data/vendor/rust/crates/j-law-core/src/types/mod.rs +11 -0
  64. data/vendor/rust/crates/j-law-core/src/types/rate.rs +219 -0
  65. data/vendor/rust/crates/j-law-core/src/types/rounding.rs +81 -0
  66. data/vendor/rust/crates/j-law-registry/Cargo.toml +15 -0
  67. data/vendor/rust/crates/j-law-registry/data/consumption_tax/consumption_tax.json +70 -0
  68. data/vendor/rust/crates/j-law-registry/data/income_tax/deductions.json +327 -0
  69. data/vendor/rust/crates/j-law-registry/data/income_tax/income_tax.json +352 -0
  70. data/vendor/rust/crates/j-law-registry/data/real_estate/brokerage_fee.json +125 -0
  71. data/vendor/rust/crates/j-law-registry/data/stamp_tax/stamp_tax.json +674 -0
  72. data/vendor/rust/crates/j-law-registry/data/withholding_tax/withholding_tax.json +70 -0
  73. data/vendor/rust/crates/j-law-registry/src/consumption_tax_loader.rs +325 -0
  74. data/vendor/rust/crates/j-law-registry/src/consumption_tax_schema.rs +49 -0
  75. data/vendor/rust/crates/j-law-registry/src/income_tax_deduction_loader.rs +636 -0
  76. data/vendor/rust/crates/j-law-registry/src/income_tax_deduction_schema.rs +111 -0
  77. data/vendor/rust/crates/j-law-registry/src/income_tax_loader.rs +445 -0
  78. data/vendor/rust/crates/j-law-registry/src/income_tax_schema.rs +44 -0
  79. data/vendor/rust/crates/j-law-registry/src/lib.rs +20 -0
  80. data/vendor/rust/crates/j-law-registry/src/loader.rs +221 -0
  81. data/vendor/rust/crates/j-law-registry/src/schema.rs +73 -0
  82. data/vendor/rust/crates/j-law-registry/src/stamp_tax_loader.rs +374 -0
  83. data/vendor/rust/crates/j-law-registry/src/stamp_tax_schema.rs +72 -0
  84. data/vendor/rust/crates/j-law-registry/src/validator.rs +204 -0
  85. data/vendor/rust/crates/j-law-registry/src/withholding_tax_loader.rs +310 -0
  86. data/vendor/rust/crates/j-law-registry/src/withholding_tax_schema.rs +61 -0
  87. 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
+ }