btctax_forms/map.rs
1//! Committed, per-(form, year) field maps: **logical cell → fully-qualified PDF field name**.
2//!
3//! The maps are DATA (TOML committed next to the bundled PDFs), not code — "adding a year" is a
4//! `forms/<year>/` directory (PDF + maps), never a code change. Keys are the fully-qualified,
5//! bracketed AcroForm names (`topmostSubform[0].Page1[0].Table_Line1_Part1[0].Row1[0].f1_03[0]`).
6//!
7//! Nothing here is trusted blindly: the geometric read-back ([`crate::verify`]) re-derives the
8//! column/row bands from the bundled PDF's own widget `/Rect`s and would flag any mis-labeled cell,
9//! and `map_2025_matches_bundled_pdf_fieldset` asserts every name here exists in the PDF.
10
11use crate::error::FormsError;
12use serde::Deserialize;
13
14/// The TY2025 Form 8949 map (embedded at compile time).
15pub const F8949_MAP_2025: &str = include_str!("../forms/2025/f8949.map.toml");
16/// The TY2025 Schedule D map (embedded at compile time).
17pub const SCHEDULE_D_MAP_2025: &str = include_str!("../forms/2025/schedule_d.map.toml");
18/// The TY2025 Schedule SE map (embedded at compile time).
19pub const SCHEDULE_SE_MAP_2025: &str = include_str!("../forms/2025/schedule_se.map.toml");
20/// The TY2025 Form 8283 map (embedded at compile time).
21pub const F8283_MAP_2025: &str = include_str!("../forms/2025/f8283.map.toml");
22/// The TY2025 Form 1040 map (embedded at compile time).
23pub const F1040_MAP_2025: &str = include_str!("../forms/2025/f1040.map.toml");
24
25/// The TY2024 Form 8949 map (embedded at compile time).
26pub const F8949_MAP_2024: &str = include_str!("../forms/2024/f8949.map.toml");
27/// The TY2024 Schedule D map (embedded at compile time).
28pub const SCHEDULE_D_MAP_2024: &str = include_str!("../forms/2024/schedule_d.map.toml");
29/// The TY2024 Schedule SE map (embedded at compile time).
30pub const SCHEDULE_SE_MAP_2024: &str = include_str!("../forms/2024/schedule_se.map.toml");
31/// The TY2024 Form 8283 map (Rev. 12-2023, embedded at compile time).
32pub const F8283_MAP_2024: &str = include_str!("../forms/2024/f8283.map.toml");
33/// The TY2024 Form 1040 map (embedded at compile time).
34pub const F1040_MAP_2024: &str = include_str!("../forms/2024/f1040.map.toml");
35
36/// The TY2017 Form 8949 map (embedded at compile time).
37pub const F8949_MAP_2017: &str = include_str!("../forms/2017/f8949.map.toml");
38/// The TY2017 Schedule D map (embedded at compile time).
39pub const SCHEDULE_D_MAP_2017: &str = include_str!("../forms/2017/schedule_d.map.toml");
40/// The TY2017 Schedule SE map (OLD short+long form; btctax fills §B long — embedded at compile time).
41pub const SCHEDULE_SE_MAP_2017: &str = include_str!("../forms/2017/schedule_se.map.toml");
42/// The TY2017 Form 8283 map (Rev. 12-2014, "j Other" — embedded at compile time).
43pub const F8283_MAP_2017: &str = include_str!("../forms/2017/f8283.map.toml");
44/// The TY2017 Form 1040 map (line 13, no DA question — embedded at compile time).
45pub const F1040_MAP_2017: &str = include_str!("../forms/2017/f1040.map.toml");
46
47/// The 4 monetary "amount" columns of a Form 8949 / Schedule D totals row: (d) proceeds, (e) cost,
48/// (g) adjustment, (h) gain. Column (f) — the code column — has no total (a spacer), so it is absent.
49#[derive(Debug, Clone, Deserialize)]
50pub struct AmountCols {
51 /// Column (d) — proceeds.
52 pub proceeds_d: String,
53 /// Column (e) — cost basis.
54 pub cost_e: String,
55 /// Column (g) — adjustment amount.
56 pub adj_g: String,
57 /// Column (h) — gain/loss.
58 pub gain_h: String,
59}
60
61/// One Form 8949 part (Part I short-term on page 0, Part II long-term on page 1).
62#[derive(Debug, Clone, Deserialize)]
63pub struct PartMap {
64 /// `"short"` (Part I) or `"long"` (Part II).
65 pub term: String,
66 /// 0-based page index of this part within the bundled 2-page PDF.
67 pub page: usize,
68 /// The digital-asset box checkbox field — **Box I** (ST) / **Box L** (LT), NOT C/F.
69 pub box_field: String,
70 /// The checkbox on-state (a PDF name without the leading `/`), e.g. `"6"`.
71 pub box_on: String,
72 /// The line-2 per-part totals row (d,e,g,h).
73 pub totals: AmountCols,
74 /// The 11 data rows; each row is the 8 column field names in order a,b,c,d,e,f,g,h.
75 pub rows: Vec<Vec<String>>,
76}
77
78/// The full Form 8949 field map for one tax year.
79#[derive(Debug, Clone, Deserialize)]
80pub struct Form8949Map {
81 /// `"f8949"`.
82 pub form: String,
83 /// Tax year (e.g. 2025).
84 pub year: i32,
85 /// Rows per part per page — **map data**, not a hard-coded constant (a new form revision that
86 /// changes the grid is a data-only edit).
87 pub rows_per_page: usize,
88 /// The data-grid subform token used to re-derive the geometry bands — **per-year map config**,
89 /// not a const (2024 = `Table_Line1`, 2025 = `Table_Line1_Part`; the row fqns differ by year).
90 pub table_token: String,
91 /// Part I then Part II.
92 pub parts: Vec<PartMap>,
93}
94
95impl Form8949Map {
96 /// Parse the committed TOML.
97 pub fn parse(toml_src: &str) -> Result<Self, toml::de::Error> {
98 toml::from_str(toml_src)
99 }
100
101 /// The TY2025 map.
102 pub fn ty2025() -> Self {
103 Self::parse(F8949_MAP_2025).expect("bundled f8949 2025 map parses")
104 }
105
106 /// The TY2024 map.
107 pub fn ty2024() -> Self {
108 Self::parse(F8949_MAP_2024).expect("bundled f8949 2024 map parses")
109 }
110
111 /// The TY2017 map (pre-1099-DA: Box C/F, `/3`; field-identical grid to 2024).
112 pub fn ty2017() -> Self {
113 Self::parse(F8949_MAP_2017).expect("bundled f8949 2017 map parses")
114 }
115
116 /// The map for a supported tax year.
117 pub fn for_year(year: i32) -> Result<Self, FormsError> {
118 match year {
119 2017 => Ok(Self::ty2017()),
120 2024 => Ok(Self::ty2024()),
121 2025 => Ok(Self::ty2025()),
122 _ => Err(FormsError::UnsupportedYear(year)),
123 }
124 }
125
126 /// The part with the given term, if present.
127 pub fn part(&self, term: &str) -> Option<&PartMap> {
128 self.parts.iter().find(|p| p.term == term)
129 }
130}
131
132/// A checkbox choice (field + on-state) — used for the Schedule D QOF Yes/No answer and the Form 1040
133/// Digital-Asset Yes/No question.
134#[derive(Debug, Clone, Deserialize)]
135pub struct CheckChoice {
136 /// The checkbox field name.
137 pub field: String,
138 /// On-state PDF name (without leading `/`).
139 pub on: String,
140}
141
142/// A dollars-field + cents-field PAIR (the 2017 Schedule SE / Form 1040 / Form 8283 split every money
143/// amount into a whole-dollars field and a 2-digit cents field). The geometric oracle treats the pair
144/// as ONE logical cell **at the dollars-field geometry** (the cents field rides along as an authorized
145/// but geometry-exempt write). Because both fields descend from the same AcroForm root, `merge_copies`
146/// (which renames only the root `/T`) rewrites BOTH names as a unit — so overflow is safe.
147#[derive(Debug, Clone, Deserialize)]
148pub struct MoneyPair {
149 /// The whole-dollars field (the one the geometry oracle checks — column-x + row/descent).
150 pub dollars_field: String,
151 /// The 2-digit cents field (an authorized write; NOT independently geometry-checked).
152 pub cents_field: String,
153}
154
155/// A monetary cell: a single field carrying the whole formatted amount (2024/2025), or a
156/// dollars+cents [`MoneyPair`] (the 2017 forms). Deserializes untagged: a TOML **string** →
157/// [`MoneyCell::Single`]; a TOML **inline table** `{ dollars_field, cents_field }` →
158/// [`MoneyCell::Pair`].
159#[derive(Debug, Clone, Deserialize)]
160#[serde(untagged)]
161pub enum MoneyCell {
162 /// A single field holding the whole formatted amount.
163 Single(String),
164 /// A dollars-field + cents-field pair.
165 Pair(MoneyPair),
166}
167
168impl MoneyCell {
169 /// Every PDF field this cell targets (1 for a single, 2 for a pair) — for coverage guards.
170 pub fn fields(&self) -> Vec<&str> {
171 match self {
172 MoneyCell::Single(f) => vec![f.as_str()],
173 MoneyCell::Pair(p) => vec![p.dollars_field.as_str(), p.cents_field.as_str()],
174 }
175 }
176}
177
178/// A per-year default: the Digital-Asset question is present unless a year's map says otherwise.
179fn default_da_present() -> bool {
180 true
181}
182
183/// The Form 1040 capital-gains field map for one tax year: the capital-gain amount cell (line 7a in
184/// 2025 / line 7 in 2024 / **line 13** in 2017) + the Digital-Asset question (absent in 2017).
185#[derive(Debug, Clone, Deserialize)]
186pub struct Form1040Map {
187 /// `"f1040"`.
188 pub form: String,
189 /// Tax year.
190 pub year: i32,
191 /// The capital-gain amount cell (line 7a for 2025, line 7 for 2024, **line 13 for 2017**). A
192 /// single field on 2024/2025; a dollars+cents [`MoneyPair`] on the 2017 form.
193 pub line7a: MoneyCell,
194 /// Whether this year's 1040 carries the Digital-Asset question — **per-year scaffolding**. When
195 /// `true` (2024/2025) the fill answers it "Yes" and runs the map-independent adjacency guard;
196 /// **2017 sets it `false`** (no DA question — the map omits `da_yes`/`da_no` and the fill produces
197 /// the 1040 iff there is reportable capital activity).
198 #[serde(default = "default_da_present")]
199 pub da_present: bool,
200 /// Digital-Asset question "Yes" (LEFT member of the adjacent pair, on-state `/1`). `None` when the
201 /// year's 1040 has no DA question (2017).
202 #[serde(default)]
203 pub da_yes: Option<CheckChoice>,
204 /// Digital-Asset question "No" (right member, on-state `/2`) — never checked by btctax. `None`
205 /// when the year's 1040 has no DA question (2017).
206 #[serde(default)]
207 pub da_no: Option<CheckChoice>,
208}
209
210impl Form1040Map {
211 /// Parse the committed TOML.
212 pub fn parse(toml_src: &str) -> Result<Self, toml::de::Error> {
213 toml::from_str(toml_src)
214 }
215
216 /// The TY2025 map.
217 pub fn ty2025() -> Self {
218 Self::parse(F1040_MAP_2025).expect("bundled f1040 2025 map parses")
219 }
220
221 /// The TY2024 map.
222 pub fn ty2024() -> Self {
223 Self::parse(F1040_MAP_2024).expect("bundled f1040 2024 map parses")
224 }
225
226 /// The TY2017 map (capital gain on line 13; NO Digital-Asset question).
227 pub fn ty2017() -> Self {
228 Self::parse(F1040_MAP_2017).expect("bundled f1040 2017 map parses")
229 }
230
231 /// The map for a supported tax year.
232 pub fn for_year(year: i32) -> Result<Self, FormsError> {
233 match year {
234 2017 => Ok(Self::ty2017()),
235 2024 => Ok(Self::ty2024()),
236 2025 => Ok(Self::ty2025()),
237 _ => Err(FormsError::UnsupportedYear(year)),
238 }
239 }
240}
241
242/// One Form 8283 **Section A** row (Donated Property of $5,000 or Less): the 8 filled columns.
243#[derive(Debug, Clone, Deserialize)]
244pub struct Section8283ARow {
245 /// (a) Name and address of the donee organization.
246 pub donee: String,
247 /// (c) Description and condition of donated property.
248 pub desc: String,
249 /// (d) Date of the contribution (full date).
250 pub date_contrib: String,
251 /// (e) Date acquired by donor (mo., yr.).
252 pub date_acq: String,
253 /// (f) How acquired by donor.
254 pub how: String,
255 /// (g) Donor's cost or adjusted basis (money — a [`MoneyPair`] on the 2017 Rev. 12-2014 form).
256 pub cost: MoneyCell,
257 /// (h) Fair market value (money — a [`MoneyPair`] on the 2017 form).
258 pub fmv: MoneyCell,
259 /// (i) Method used to determine the FMV.
260 pub method: String,
261}
262
263/// Form 8283 Section A (page 1, Line 1) — up to 4 rows A–D.
264#[derive(Debug, Clone, Deserialize)]
265pub struct Section8283A {
266 /// The 4 rows A–D.
267 pub rows: Vec<Section8283ARow>,
268}
269
270/// One Form 8283 **Section B Part I** row (Over $5,000): the filled columns.
271#[derive(Debug, Clone, Deserialize)]
272pub struct Section8283BRow {
273 /// (a) Description of donated property.
274 pub desc: String,
275 /// (c) Appraised fair market value (money — a [`MoneyPair`] on the 2017 Rev. 12-2014 form).
276 pub fmv: MoneyCell,
277 /// (d) Date acquired by donor (mo., yr.).
278 pub date_acq: String,
279 /// (e) How acquired by donor.
280 pub how: String,
281 /// (f) Donor's cost or adjusted basis (money — a [`MoneyPair`] on the 2017 form).
282 pub cost: MoneyCell,
283 /// (i)/(h) Amount claimed as a deduction (carrier row only; money — a [`MoneyPair`] on 2017).
284 pub deduction: MoneyCell,
285}
286
287/// Form 8283 Section B (page 1/2, over-$5,000 property + page 2 identity) — up to 3 rows (2024/2025)
288/// or 4 rows (2017 Rev. 12-2014, `Line5A`–`Line5D`).
289#[derive(Debug, Clone, Deserialize)]
290pub struct Section8283B {
291 /// The property-type checkbox MUST be checked for BTC: **"k Digital assets"** (on-state `/11`) on
292 /// the Rev. 12-2023/2025 forms; the Rev. 12-2014 form has no digital-asset box, so 2017 uses
293 /// **"j Other"** (on-state `/9`) plus [`Self::btc_property_note`].
294 pub k_digital_assets: CheckChoice,
295 /// 2017 only: since "j Other" gives no category, the digital-asset nature is identified by a
296 /// printed note **prepended to the first row's (a) description** (e.g. "Other property: digital
297 /// asset (virtual currency)"). `None` on 2024/2025 ("k Digital assets" is self-describing).
298 #[serde(default)]
299 pub btc_property_note: Option<String>,
300 /// Part IV/III appraiser name (page 2). `None` when the revision has no printed-name field (the
301 /// Rev. 12-2014 form: the appraiser identity is the handwritten signature, left blank).
302 #[serde(default)]
303 pub appraiser_name: Option<String>,
304 /// Appraiser business address (page 2).
305 pub appraiser_address: String,
306 /// Appraiser identifying number (TIN/PTIN, page 2).
307 pub appraiser_tin: String,
308 /// Donee organization name (page 2).
309 pub donee_name: String,
310 /// Donee EIN (page 2).
311 pub donee_ein: String,
312 /// Donee address (page 2).
313 pub donee_address: String,
314 /// The rows (3 on 2024/2025, 4 on 2017) — the row count also sets the per-copy overflow cap.
315 pub rows: Vec<Section8283BRow>,
316}
317
318/// The Form 8283 (Rev. 12-2025) field map for one tax year.
319#[derive(Debug, Clone, Deserialize)]
320pub struct Form8283Map {
321 /// `"f8283"`.
322 pub form: String,
323 /// Tax year.
324 pub year: i32,
325 /// Section A (≤ $5,000).
326 pub section_a: Section8283A,
327 /// Section B (> $5,000).
328 pub section_b: Section8283B,
329}
330
331impl Form8283Map {
332 /// Parse the committed TOML.
333 pub fn parse(toml_src: &str) -> Result<Self, toml::de::Error> {
334 toml::from_str(toml_src)
335 }
336
337 /// The TY2025 map.
338 pub fn ty2025() -> Self {
339 Self::parse(F8283_MAP_2025).expect("bundled f8283 2025 map parses")
340 }
341
342 /// The TY2024 map (Form 8283 Rev. 12-2023).
343 pub fn ty2024() -> Self {
344 Self::parse(F8283_MAP_2024).expect("bundled f8283 2024 map parses")
345 }
346
347 /// The TY2017 map (Form 8283 Rev. 12-2014 — "j Other", no DA box, 5/4 rows, ¢-pairs).
348 pub fn ty2017() -> Self {
349 Self::parse(F8283_MAP_2017).expect("bundled f8283 2017 map parses")
350 }
351
352 /// The map for a supported tax year.
353 pub fn for_year(year: i32) -> Result<Self, FormsError> {
354 match year {
355 2017 => Ok(Self::ty2017()),
356 2024 => Ok(Self::ty2024()),
357 2025 => Ok(Self::ty2025()),
358 _ => Err(FormsError::UnsupportedYear(year)),
359 }
360 }
361
362 /// Every field name the map targets (for the `map_YYYY_matches_bundled_pdf_fieldset` guard).
363 pub fn field_names(&self) -> Vec<&str> {
364 let mut v = Vec::new();
365 for r in &self.section_a.rows {
366 v.extend([
367 r.donee.as_str(),
368 r.desc.as_str(),
369 r.date_contrib.as_str(),
370 r.date_acq.as_str(),
371 r.how.as_str(),
372 ]);
373 v.extend(r.cost.fields());
374 v.extend(r.fmv.fields());
375 v.push(r.method.as_str());
376 }
377 let b = &self.section_b;
378 v.push(b.k_digital_assets.field.as_str());
379 if let Some(n) = &b.appraiser_name {
380 v.push(n.as_str());
381 }
382 v.extend([
383 b.appraiser_address.as_str(),
384 b.appraiser_tin.as_str(),
385 b.donee_name.as_str(),
386 b.donee_ein.as_str(),
387 b.donee_address.as_str(),
388 ]);
389 for r in &b.rows {
390 v.extend([r.desc.as_str(), r.date_acq.as_str(), r.how.as_str()]);
391 v.extend(r.fmv.fields());
392 v.extend(r.cost.fields());
393 v.extend(r.deduction.fields());
394 }
395 v
396 }
397}
398
399/// The Schedule D field map for one tax year.
400#[derive(Debug, Clone, Deserialize)]
401pub struct ScheduleDMap {
402 /// `"schedule_d"`.
403 pub form: String,
404 /// Tax year.
405 pub year: i32,
406 /// Line 3 — Part I total from Form 8949 (Box C **or Box I**): columns d,e,g,h.
407 pub line3: AmountCols,
408 /// Line 7 — net short-term gain/loss (column h).
409 pub line7_h: String,
410 /// Line 10 — Part II total from Form 8949 (Box F **or Box L**): columns d,e,g,h.
411 pub line10: AmountCols,
412 /// Line 15 — net long-term gain/loss (column h).
413 pub line15_h: String,
414 /// Line 16 — total (line 7 + line 15), column h, page 2.
415 pub line16_h: String,
416 /// The Part I amount-column subform token used to re-derive the geometry bands — **per-year map
417 /// config** (`Table_PartI` for 2024/2025, **`TablePartI`** (no underscore) for the 2017 form).
418 #[serde(default = "default_sched_d_token")]
419 pub table_token: String,
420 /// QOF question "Yes" choice. `None` on years whose Schedule D has no QOF question (2017 —
421 /// Qualified Opportunity Funds began in 2019).
422 #[serde(default)]
423 pub qof_yes: Option<CheckChoice>,
424 /// QOF question "No" choice (answered No when present). `None` on 2017 (no QOF question).
425 #[serde(default)]
426 pub qof_no: Option<CheckChoice>,
427}
428
429/// The default Schedule D Part I grid token (2024/2025); the 2017 map overrides it to `TablePartI`.
430fn default_sched_d_token() -> String {
431 "Table_PartI".to_string()
432}
433
434impl ScheduleDMap {
435 /// Parse the committed TOML.
436 pub fn parse(toml_src: &str) -> Result<Self, toml::de::Error> {
437 toml::from_str(toml_src)
438 }
439
440 /// The TY2025 map.
441 pub fn ty2025() -> Self {
442 Self::parse(SCHEDULE_D_MAP_2025).expect("bundled schedule_d 2025 map parses")
443 }
444
445 /// The TY2024 map.
446 pub fn ty2024() -> Self {
447 Self::parse(SCHEDULE_D_MAP_2024).expect("bundled schedule_d 2024 map parses")
448 }
449
450 /// The TY2017 map (grid token `TablePartI`; NO QOF question).
451 pub fn ty2017() -> Self {
452 Self::parse(SCHEDULE_D_MAP_2017).expect("bundled schedule_d 2017 map parses")
453 }
454
455 /// The map for a supported tax year.
456 pub fn for_year(year: i32) -> Result<Self, FormsError> {
457 match year {
458 2017 => Ok(Self::ty2017()),
459 2024 => Ok(Self::ty2024()),
460 2025 => Ok(Self::ty2025()),
461 _ => Err(FormsError::UnsupportedYear(year)),
462 }
463 }
464}
465
466/// The Schedule SE (Form 1040) field map for one tax year — the filled §1401 line chain.
467#[derive(Debug, Clone, Deserialize)]
468pub struct ScheduleSeMap {
469 /// `"schedule_se"`.
470 pub form: String,
471 /// Tax year.
472 pub year: i32,
473 /// Line 2 — net profit (net_se), amount column.
474 pub line2: MoneyCell,
475 /// Line 3 — combine 1a/1b/2 (= line 2), amount column.
476 pub line3: MoneyCell,
477 /// Line 4a — net SE earnings (base = net_se × 92.35%), amount column.
478 pub line4a: MoneyCell,
479 /// Line 4c — combine 4a/4b (= line 4a), amount column. The $400 STOP threshold.
480 pub line4c: MoneyCell,
481 /// Line 6 — add 4c/5b (= line 4c), amount column.
482 pub line6: MoneyCell,
483 /// Line 8a — Form W-2 Social Security wages, **MID column**.
484 pub line8a: MoneyCell,
485 /// Line 8d — add 8a/8b/8c (= line 8a), amount column.
486 pub line8d: MoneyCell,
487 /// Line 9 — line 7 (`ss_wage_base` constant) − line 8d, amount column.
488 pub line9: MoneyCell,
489 /// Line 10 — Social Security portion (`ss`), amount column.
490 pub line10: MoneyCell,
491 /// Line 11 — regular Medicare portion (`medicare`), amount column.
492 pub line11: MoneyCell,
493 /// Line 12 — SE tax = line 10 + line 11 (**SS + regular Medicare ONLY**), amount column.
494 pub line12: MoneyCell,
495 /// Line 13 — one-half SE-tax deduction (= line 12 × 50% = `deductible_half`), **MID column**.
496 pub line13: MoneyCell,
497 /// Fields the BLANK form already carries a factory `/V` for (the 2017 §B long form pre-prints
498 /// line 7 = `127,200`/`00` and line 14 = `5,200`/`00`) — excluded from the `no_unmapped_filled`
499 /// guard so those constants don't read as stray writes. Empty on 2024/2025.
500 #[serde(default)]
501 pub prefilled_exempt: Vec<String>,
502}
503
504impl ScheduleSeMap {
505 /// Parse the committed TOML.
506 pub fn parse(toml_src: &str) -> Result<Self, toml::de::Error> {
507 toml::from_str(toml_src)
508 }
509
510 /// The TY2025 map.
511 pub fn ty2025() -> Self {
512 Self::parse(SCHEDULE_SE_MAP_2025).expect("bundled schedule_se 2025 map parses")
513 }
514
515 /// The TY2024 map (field-name-identical to 2025; only the wage base differs).
516 pub fn ty2024() -> Self {
517 Self::parse(SCHEDULE_SE_MAP_2024).expect("bundled schedule_se 2024 map parses")
518 }
519
520 /// The TY2017 map (OLD §B long form: dollars+cents pairs; pre-filled line 7/14 exempt).
521 pub fn ty2017() -> Self {
522 Self::parse(SCHEDULE_SE_MAP_2017).expect("bundled schedule_se 2017 map parses")
523 }
524
525 /// The map for a supported tax year.
526 pub fn for_year(year: i32) -> Result<Self, FormsError> {
527 match year {
528 2017 => Ok(Self::ty2017()),
529 2024 => Ok(Self::ty2024()),
530 2025 => Ok(Self::ty2025()),
531 _ => Err(FormsError::UnsupportedYear(year)),
532 }
533 }
534
535 /// The 12 filled line cells, in chain order.
536 pub fn lines(&self) -> [&MoneyCell; 12] {
537 [
538 &self.line2,
539 &self.line3,
540 &self.line4a,
541 &self.line4c,
542 &self.line6,
543 &self.line8a,
544 &self.line8d,
545 &self.line9,
546 &self.line10,
547 &self.line11,
548 &self.line12,
549 &self.line13,
550 ]
551 }
552
553 /// Every field name the map targets (for the `map_YYYY_matches_bundled_pdf_fieldset` guard) —
554 /// both members of each dollars+cents pair on the 2017 form.
555 pub fn field_names(&self) -> Vec<&str> {
556 self.lines().iter().flat_map(|c| c.fields()).collect()
557 }
558}