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,171 @@
1
+ use thiserror::Error;
2
+
3
+ /// Registry(告示JSONデータ)の不整合エラー。
4
+ /// 起動時バリデーションで検出し、panic! で伝播させてよい層。
5
+ #[derive(Debug, Error)]
6
+ pub enum RegistryError {
7
+ #[error("法令データに適用期間の重複があります: domain={domain}, from={from}, until={until}")]
8
+ PeriodOverlap {
9
+ domain: String,
10
+ from: String,
11
+ until: String,
12
+ },
13
+
14
+ #[error(
15
+ "法令データに適用期間の空白があります: domain={domain}, end={end}, next_start={next_start}"
16
+ )]
17
+ PeriodGap {
18
+ domain: String,
19
+ end: String,
20
+ next_start: String,
21
+ },
22
+
23
+ #[error("分母にゼロが含まれています: path={path}")]
24
+ ZeroDenominator { path: String },
25
+
26
+ #[error("日付の形式が不正です: domain={domain}, value={value}")]
27
+ InvalidDateFormat { domain: String, value: String },
28
+
29
+ #[error("JSONファイルが見つかりません: {path}")]
30
+ FileNotFound { path: String },
31
+
32
+ #[error("JSONのパースに失敗しました: path={path}, cause={cause}")]
33
+ ParseError { path: String, cause: String },
34
+ }
35
+
36
+ /// ユーザー入力の不正エラー。
37
+ #[derive(Debug, Error)]
38
+ pub enum InputError {
39
+ #[error("負の金額は無効です: value={value}")]
40
+ NegativeAmount { value: i64 },
41
+
42
+ #[error("無効な日付です: date={date}, reason={reason}")]
43
+ InvalidDate { date: String, reason: String },
44
+
45
+ #[error("指定した日付は法令の適用期間外です: date={date}")]
46
+ DateOutOfRange { date: String },
47
+
48
+ #[error("矛盾するフラグが同時に指定されています: {flag_a} と {flag_b}")]
49
+ ConflictingFlags { flag_a: String, flag_b: String },
50
+
51
+ #[error("所得控除の入力が無効です: field={field}, reason={reason}")]
52
+ InvalidDeductionInput { field: String, reason: String },
53
+
54
+ #[error("源泉徴収入力が無効です: field={field}, reason={reason}")]
55
+ InvalidWithholdingInput { field: String, reason: String },
56
+
57
+ #[error("印紙税入力が無効です: field={field}, reason={reason}")]
58
+ InvalidStampTaxInput { field: String, reason: String },
59
+
60
+ #[error("分母にゼロが指定されました")]
61
+ ZeroDenominator,
62
+ }
63
+
64
+ /// 計算処理中の異常エラー。
65
+ #[derive(Debug, Error)]
66
+ pub enum CalculationError {
67
+ #[error("計算中に整数オーバーフローが発生しました: step={step}")]
68
+ Overflow { step: String },
69
+
70
+ #[error("このコンテキストにポリシーを適用できません: {reason}")]
71
+ PolicyNotApplicable { reason: String },
72
+ }
73
+
74
+ /// J-Law-Core 全体のトップレベルエラー型。
75
+ #[derive(Debug, Error)]
76
+ pub enum JLawError {
77
+ #[error(transparent)]
78
+ Registry(#[from] RegistryError),
79
+
80
+ #[error(transparent)]
81
+ Input(#[from] InputError),
82
+
83
+ #[error(transparent)]
84
+ Calculation(#[from] CalculationError),
85
+ }
86
+
87
+ #[cfg(test)]
88
+ mod tests {
89
+ use super::*;
90
+
91
+ #[test]
92
+ fn registry_error_display() {
93
+ let e = RegistryError::PeriodOverlap {
94
+ domain: "real_estate".into(),
95
+ from: "2024-01-01".into(),
96
+ until: "2024-12-31".into(),
97
+ };
98
+ assert!(e.to_string().contains("real_estate"));
99
+ assert!(e.to_string().contains("重複"));
100
+ }
101
+
102
+ #[test]
103
+ fn input_error_display() {
104
+ let e = InputError::NegativeAmount { value: -100 };
105
+ assert!(e.to_string().contains("-100"));
106
+ }
107
+
108
+ #[test]
109
+ fn invalid_date_display() {
110
+ let e = InputError::InvalidDate {
111
+ date: "2024-02-30".into(),
112
+ reason: "指定された月の日数を超えています".into(),
113
+ };
114
+ assert!(e.to_string().contains("2024-02-30"));
115
+ assert!(e.to_string().contains("指定された月の日数"));
116
+ }
117
+
118
+ #[test]
119
+ fn invalid_deduction_input_display() {
120
+ let e = InputError::InvalidDeductionInput {
121
+ field: "social_insurance_premium_paid".into(),
122
+ reason: "test".into(),
123
+ };
124
+ assert!(e.to_string().contains("social_insurance_premium_paid"));
125
+ assert!(e.to_string().contains("test"));
126
+ }
127
+
128
+ #[test]
129
+ fn invalid_withholding_input_display() {
130
+ let e = InputError::InvalidWithholdingInput {
131
+ field: "category".into(),
132
+ reason: "unknown".into(),
133
+ };
134
+ assert!(e.to_string().contains("category"));
135
+ assert!(e.to_string().contains("unknown"));
136
+ }
137
+
138
+ #[test]
139
+ fn invalid_stamp_tax_input_display() {
140
+ let e = InputError::InvalidStampTaxInput {
141
+ field: "document_code".into(),
142
+ reason: "unknown".into(),
143
+ };
144
+ assert!(e.to_string().contains("document_code"));
145
+ assert!(e.to_string().contains("unknown"));
146
+ }
147
+
148
+ #[test]
149
+ fn calculation_error_display() {
150
+ let e = CalculationError::Overflow {
151
+ step: "tier1".into(),
152
+ };
153
+ assert!(e.to_string().contains("tier1"));
154
+ }
155
+
156
+ #[test]
157
+ fn jlaw_error_from_input() {
158
+ let inner = InputError::ZeroDenominator;
159
+ let outer: JLawError = inner.into();
160
+ assert!(matches!(outer, JLawError::Input(_)));
161
+ }
162
+
163
+ #[test]
164
+ fn jlaw_error_from_calculation() {
165
+ let inner = CalculationError::PolicyNotApplicable {
166
+ reason: "test".into(),
167
+ };
168
+ let outer: JLawError = inner.into();
169
+ assert!(matches!(outer, JLawError::Calculation(_)));
170
+ }
171
+ }
@@ -0,0 +1,9 @@
1
+ pub mod domains;
2
+ pub mod error;
3
+ pub mod types;
4
+
5
+ pub use error::{CalculationError, InputError, JLawError, RegistryError};
6
+ pub use types::{
7
+ FinalAmount, IntermediateAmount, LegalCitation, LegalDate, MultiplyOrder, Rate,
8
+ RoundingStrategy,
9
+ };
@@ -0,0 +1,232 @@
1
+ use crate::error::InputError;
2
+ use crate::types::rounding::RoundingStrategy;
3
+
4
+ /// 計算の最終結果を表す金額型(円単位・整数)。
5
+ ///
6
+ /// 税込合計・税抜合計・税額など、ユーザーに返す確定値にのみ使う。
7
+ /// 計算途中では [`IntermediateAmount`] を使うこと。
8
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
9
+ pub struct FinalAmount(u64);
10
+
11
+ impl FinalAmount {
12
+ /// 円単位の値から `FinalAmount` を作る。
13
+ pub fn new(yen: u64) -> Self {
14
+ Self(yen)
15
+ }
16
+
17
+ /// 円単位の値を返す。
18
+ pub fn as_yen(self) -> u64 {
19
+ self.0
20
+ }
21
+ }
22
+
23
+ impl std::fmt::Display for FinalAmount {
24
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25
+ write!(f, "{}円", self.0)
26
+ }
27
+ }
28
+
29
+ /// 計算途中の金額を分数で表す型。
30
+ ///
31
+ /// `whole + numer/denom` を表す。例えば `100 + 1/3` は
32
+ /// `IntermediateAmount { whole: 100, numer: 1, denom: 3 }`。
33
+ ///
34
+ /// 端数処理が必要な場面では [`IntermediateAmount::finalize`] を呼ぶこと。
35
+ #[derive(Debug, Clone, PartialEq, Eq)]
36
+ pub struct IntermediateAmount {
37
+ /// 整数部分(円)。
38
+ pub(crate) whole: u64,
39
+ /// 分子。
40
+ pub(crate) numer: u64,
41
+ /// 分母。`0` は不正値であり、コンストラクタで拒否される。
42
+ pub(crate) denom: u64,
43
+ }
44
+
45
+ impl IntermediateAmount {
46
+ /// 整数値(円)から `IntermediateAmount` を作る(端数なし)。
47
+ pub fn from_exact(yen: u64) -> Self {
48
+ Self {
49
+ whole: yen,
50
+ numer: 0,
51
+ denom: 1,
52
+ }
53
+ }
54
+
55
+ /// 分数形式で作る。`denom == 0` の場合はエラーを返す。
56
+ pub fn try_new(whole: u64, numer: u64, denom: u64) -> Result<Self, InputError> {
57
+ if denom == 0 {
58
+ return Err(InputError::ZeroDenominator);
59
+ }
60
+ Ok(Self {
61
+ whole,
62
+ numer,
63
+ denom,
64
+ })
65
+ }
66
+
67
+ /// 端数処理して [`FinalAmount`] に変換する。
68
+ ///
69
+ /// # エラー
70
+ /// `self.denom == 0` の場合は `InputError::ZeroDenominator` を返す。
71
+ pub fn finalize(&self, rounding: RoundingStrategy) -> Result<FinalAmount, InputError> {
72
+ let frac = if self.numer == 0 {
73
+ 0
74
+ } else {
75
+ rounding.apply_ratio(self.numer, self.denom)?
76
+ };
77
+ Ok(FinalAmount::new(self.whole + frac))
78
+ }
79
+
80
+ /// 整数部分(円)を返す。
81
+ pub fn whole(&self) -> u64 {
82
+ self.whole
83
+ }
84
+
85
+ /// 分子を返す。
86
+ pub fn numer(&self) -> u64 {
87
+ self.numer
88
+ }
89
+
90
+ /// 分母を返す。
91
+ pub fn denom(&self) -> u64 {
92
+ self.denom
93
+ }
94
+ }
95
+
96
+ /// `IntermediateAmount` 同士の加算を標準トレイトで実装。
97
+ impl std::ops::Add<&IntermediateAmount> for IntermediateAmount {
98
+ type Output = IntermediateAmount;
99
+
100
+ /// 加算(整数部分同士を加える)。
101
+ ///
102
+ /// 通分計算 `(a.numer * b.denom + b.numer * a.denom) / (a.denom * b.denom)` において
103
+ /// 乗算・加算がオーバーフローした場合は `whole = u64::MAX` をセットして返す。
104
+ /// 法令計算で扱う金額の範囲では通常発生しないが、
105
+ /// 異常入力に対して silently panic しないための安全対策。
106
+ fn add(self, other: &IntermediateAmount) -> IntermediateAmount {
107
+ // 通分: frac 部は (a.numer * b.denom + b.numer * a.denom) / (a.denom * b.denom)
108
+ let Some(new_denom) = self.denom.checked_mul(other.denom) else {
109
+ return IntermediateAmount {
110
+ whole: u64::MAX,
111
+ numer: 0,
112
+ denom: 1,
113
+ };
114
+ };
115
+ let Some(lhs) = self.numer.checked_mul(other.denom) else {
116
+ return IntermediateAmount {
117
+ whole: u64::MAX,
118
+ numer: 0,
119
+ denom: 1,
120
+ };
121
+ };
122
+ let Some(rhs) = other.numer.checked_mul(self.denom) else {
123
+ return IntermediateAmount {
124
+ whole: u64::MAX,
125
+ numer: 0,
126
+ denom: 1,
127
+ };
128
+ };
129
+ let Some(new_numer) = lhs.checked_add(rhs) else {
130
+ return IntermediateAmount {
131
+ whole: u64::MAX,
132
+ numer: 0,
133
+ denom: 1,
134
+ };
135
+ };
136
+ // 整数部を繰り上げながら正規化
137
+ let carry = new_numer / new_denom;
138
+ let rem = new_numer % new_denom;
139
+ let Some(whole) = self
140
+ .whole
141
+ .checked_add(other.whole)
142
+ .and_then(|w| w.checked_add(carry))
143
+ else {
144
+ return IntermediateAmount {
145
+ whole: u64::MAX,
146
+ numer: 0,
147
+ denom: 1,
148
+ };
149
+ };
150
+ IntermediateAmount {
151
+ whole,
152
+ numer: rem,
153
+ denom: new_denom,
154
+ }
155
+ }
156
+ }
157
+
158
+ #[cfg(test)]
159
+ #[allow(clippy::disallowed_methods)]
160
+ mod tests {
161
+ use super::*;
162
+ use std::ops::Add;
163
+
164
+ #[test]
165
+ fn final_amount_roundtrip() {
166
+ let a = FinalAmount::new(210_000);
167
+ assert_eq!(a.as_yen(), 210_000);
168
+ }
169
+
170
+ #[test]
171
+ fn from_exact_has_no_fraction() {
172
+ let a = IntermediateAmount::from_exact(100);
173
+ assert_eq!(a.whole(), 100);
174
+ assert_eq!(a.numer(), 0);
175
+ }
176
+
177
+ #[test]
178
+ fn try_new_rejects_zero_denom() {
179
+ let result = IntermediateAmount::try_new(100, 1, 0);
180
+ assert!(matches!(result, Err(InputError::ZeroDenominator)));
181
+ }
182
+
183
+ #[test]
184
+ fn finalize_floor() {
185
+ // 100 + 1/3 → Floor → 100
186
+ let a = IntermediateAmount::try_new(100, 1, 3).unwrap();
187
+ assert_eq!(a.finalize(RoundingStrategy::Floor).unwrap().as_yen(), 100);
188
+ }
189
+
190
+ #[test]
191
+ fn finalize_ceil() {
192
+ // 100 + 1/3 → Ceil → 101
193
+ let a = IntermediateAmount::try_new(100, 1, 3).unwrap();
194
+ assert_eq!(a.finalize(RoundingStrategy::Ceil).unwrap().as_yen(), 101);
195
+ }
196
+
197
+ #[test]
198
+ fn finalize_half_up() {
199
+ // 100 + 1/2 = 100.5 → HalfUp → 101
200
+ let a = IntermediateAmount::try_new(100, 1, 2).unwrap();
201
+ assert_eq!(a.finalize(RoundingStrategy::HalfUp).unwrap().as_yen(), 101);
202
+
203
+ // 100 + 1/3 = 100.333 → HalfUp → 100
204
+ let b = IntermediateAmount::try_new(100, 1, 3).unwrap();
205
+ assert_eq!(b.finalize(RoundingStrategy::HalfUp).unwrap().as_yen(), 100);
206
+ }
207
+
208
+ #[test]
209
+ fn finalize_no_fraction() {
210
+ let a = IntermediateAmount::from_exact(5_000);
211
+ assert_eq!(a.finalize(RoundingStrategy::Floor).unwrap().as_yen(), 5_000);
212
+ }
213
+
214
+ #[test]
215
+ fn add_two_exact() {
216
+ let a = IntermediateAmount::from_exact(100);
217
+ let b = IntermediateAmount::from_exact(200);
218
+ let c = a.add(&b);
219
+ assert_eq!(c.finalize(RoundingStrategy::Floor).unwrap().as_yen(), 300);
220
+ }
221
+
222
+ #[test]
223
+ fn add_carries_fraction() {
224
+ // 0 + 2/3 + 0 + 2/3 = 4/3 → carry 1, rem 1/3
225
+ let a = IntermediateAmount::try_new(0, 2, 3).unwrap();
226
+ let b = IntermediateAmount::try_new(0, 2, 3).unwrap();
227
+ let c = a.add(&b);
228
+ assert_eq!(c.whole(), 1);
229
+ // finalize Floor → 1
230
+ assert_eq!(c.finalize(RoundingStrategy::Floor).unwrap().as_yen(), 1);
231
+ }
232
+ }
@@ -0,0 +1,82 @@
1
+ use crate::types::date::LegalDate;
2
+
3
+ /// 法令の条文参照情報。
4
+ ///
5
+ /// `pub` な型・関数の docコメントに埋め込むことで、
6
+ /// 実装の根拠を機械可読な形で記録する。
7
+ #[derive(Debug, Clone, PartialEq, Eq)]
8
+ pub struct LegalCitation {
9
+ /// 法令識別子(e.g., `"reitaku-46"`, `"shotoku-tax-36"`)。
10
+ pub law_id: String,
11
+ /// 法令の正式名称。
12
+ pub law_name: String,
13
+ /// 条番号。
14
+ pub article: u16,
15
+ /// 項番号(省略可)。
16
+ pub paragraph: Option<u16>,
17
+ /// 号番号(省略可)。
18
+ pub item: Option<u16>,
19
+ /// 施行日。
20
+ pub effective_date: LegalDate,
21
+ }
22
+
23
+ impl LegalCitation {
24
+ /// 簡易コンストラクタ(条のみ指定)。
25
+ pub fn article_only(
26
+ law_id: &str,
27
+ law_name: &str,
28
+ article: u16,
29
+ effective_date: LegalDate,
30
+ ) -> Self {
31
+ Self {
32
+ law_id: law_id.to_owned(),
33
+ law_name: law_name.to_owned(),
34
+ article,
35
+ paragraph: None,
36
+ item: None,
37
+ effective_date,
38
+ }
39
+ }
40
+ }
41
+
42
+ impl std::fmt::Display for LegalCitation {
43
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44
+ write!(f, "{} 第{}条", self.law_name, self.article)?;
45
+ if let Some(p) = self.paragraph {
46
+ write!(f, "第{}項", p)?;
47
+ }
48
+ if let Some(i) = self.item {
49
+ write!(f, "第{}号", i)?;
50
+ }
51
+ Ok(())
52
+ }
53
+ }
54
+
55
+ #[cfg(test)]
56
+ mod tests {
57
+ use super::*;
58
+
59
+ #[test]
60
+ fn display_article_only() {
61
+ let c = LegalCitation::article_only(
62
+ "reitaku-46",
63
+ "宅地建物取引業法",
64
+ 46,
65
+ LegalDate::new(2024, 7, 1),
66
+ );
67
+ assert_eq!(c.to_string(), "宅地建物取引業法 第46条");
68
+ }
69
+
70
+ #[test]
71
+ fn display_with_paragraph_and_item() {
72
+ let c = LegalCitation {
73
+ law_id: "reitaku-46".into(),
74
+ law_name: "宅地建物取引業法".into(),
75
+ article: 46,
76
+ paragraph: Some(1),
77
+ item: Some(2),
78
+ effective_date: LegalDate::new(2024, 7, 1),
79
+ };
80
+ assert_eq!(c.to_string(), "宅地建物取引業法 第46条第1項第2号");
81
+ }
82
+ }