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 two identity cells every IRS form carries at its top: the name line and the SSN.
15///
16/// **Required** on the nine full-return schedule maps (a map without it fails at DESERIALIZATION —
17/// fail-closed at load, and every map is loaded by a test), and `Option` on the two maps shared with
18/// the crypto slice (`ScheduleDMap`, `Form1040Map`), whose 2017/2025 editions have no verified identity
19/// FQNs and no `ReturnInputs` to source an identity from. The full-return fillers refuse on `None`.
20///
21/// The SSN's RENDERING is not fixed here: it is chosen per-cell from the PDF's own `/MaxLen` (11 ⇒
22/// hyphenated, 9 ⇒ bare digits — the schedules and the 1040 genuinely differ). See
23/// [`crate::cells::push_identity`].
24#[derive(Debug, Clone, Deserialize)]
25pub struct IdentityCells {
26 /// "Name(s) shown on return" — or, on Schedule C, "Name of proprietor".
27 pub name: String,
28 /// The SSN cell.
29 pub ssn: String,
30}
31
32/// The TY2025 Form 8949 map (embedded at compile time).
33pub const F8949_MAP_2025: &str = include_str!("../forms/2025/f8949.map.toml");
34/// The TY2025 Schedule D map (embedded at compile time).
35pub const SCHEDULE_D_MAP_2025: &str = include_str!("../forms/2025/schedule_d.map.toml");
36/// The TY2025 Schedule SE map (embedded at compile time).
37pub const SCHEDULE_SE_MAP_2025: &str = include_str!("../forms/2025/schedule_se.map.toml");
38/// The TY2025 Form 8283 map (embedded at compile time).
39pub const F8283_MAP_2025: &str = include_str!("../forms/2025/f8283.map.toml");
40/// The TY2025 Form 1040 map (embedded at compile time).
41pub const F1040_MAP_2025: &str = include_str!("../forms/2025/f1040.map.toml");
42
43/// The TY2024 Form 8949 map (embedded at compile time).
44pub const F8949_MAP_2024: &str = include_str!("../forms/2024/f8949.map.toml");
45/// The TY2024 Schedule D map (embedded at compile time).
46pub const SCHEDULE_D_MAP_2024: &str = include_str!("../forms/2024/schedule_d.map.toml");
47/// The TY2024 Schedule SE map (embedded at compile time).
48pub const SCHEDULE_SE_MAP_2024: &str = include_str!("../forms/2024/schedule_se.map.toml");
49/// The TY2024 Form 8283 map (Rev. 12-2023, embedded at compile time).
50pub const F8283_MAP_2024: &str = include_str!("../forms/2024/f8283.map.toml");
51/// The TY2024 Form 1040 map (embedded at compile time).
52pub const F1040_MAP_2024: &str = include_str!("../forms/2024/f1040.map.toml");
53/// The TY2024 Form 8959 (Additional Medicare Tax) map (embedded at compile time).
54pub const F8959_MAP_2024: &str = include_str!("../forms/2024/f8959.map.toml");
55/// The TY2024 Form 8960 (Net Investment Income Tax) map (embedded at compile time).
56pub const F8960_MAP_2024: &str = include_str!("../forms/2024/f8960.map.toml");
57/// The TY2024 Form 8995 (QBI deduction, simplified) map (embedded at compile time).
58pub const F8995_MAP_2024: &str = include_str!("../forms/2024/f8995.map.toml");
59/// The TY2024 Schedule 2 (Additional Taxes) map (embedded at compile time).
60pub const SCHEDULE_2_MAP_2024: &str = include_str!("../forms/2024/f1040s2.map.toml");
61/// The TY2024 Schedule 3 (Additional Credits and Payments) map (embedded at compile time).
62pub const SCHEDULE_3_MAP_2024: &str = include_str!("../forms/2024/f1040s3.map.toml");
63/// The TY2024 Schedule A (Itemized Deductions) map (embedded at compile time).
64pub const SCHEDULE_A_MAP_2024: &str = include_str!("../forms/2024/f1040sa.map.toml");
65/// The TY2024 Schedule 1 (Additional Income and Adjustments) map (embedded at compile time).
66pub const SCHEDULE_1_MAP_2024: &str = include_str!("../forms/2024/f1040s1.map.toml");
67/// The TY2024 Schedule C (Profit or Loss From Business) map (embedded at compile time).
68pub const SCHEDULE_C_MAP_2024: &str = include_str!("../forms/2024/f1040sc.map.toml");
69/// The TY2024 Schedule B (Interest and Ordinary Dividends) map (embedded at compile time).
70pub const SCHEDULE_B_MAP_2024: &str = include_str!("../forms/2024/f1040sb.map.toml");
71
72/// The TY2017 Form 8949 map (embedded at compile time).
73pub const F8949_MAP_2017: &str = include_str!("../forms/2017/f8949.map.toml");
74/// The TY2017 Schedule D map (embedded at compile time).
75pub const SCHEDULE_D_MAP_2017: &str = include_str!("../forms/2017/schedule_d.map.toml");
76/// The TY2017 Schedule SE map (OLD short+long form; btctax fills §B long — embedded at compile time).
77pub const SCHEDULE_SE_MAP_2017: &str = include_str!("../forms/2017/schedule_se.map.toml");
78/// The TY2017 Form 8283 map (Rev. 12-2014, "j Other" — embedded at compile time).
79pub const F8283_MAP_2017: &str = include_str!("../forms/2017/f8283.map.toml");
80/// The TY2017 Form 1040 map (line 13, no DA question — embedded at compile time).
81pub const F1040_MAP_2017: &str = include_str!("../forms/2017/f1040.map.toml");
82
83/// The 4 monetary "amount" columns of a Form 8949 / Schedule D totals row: (d) proceeds, (e) cost,
84/// (g) adjustment, (h) gain. Column (f) — the code column — has no total (a spacer), so it is absent.
85#[derive(Debug, Clone, Deserialize)]
86pub struct AmountCols {
87 /// Column (d) — proceeds.
88 pub proceeds_d: String,
89 /// Column (e) — cost basis.
90 pub cost_e: String,
91 /// Column (g) — adjustment amount.
92 pub adj_g: String,
93 /// Column (h) — gain/loss.
94 pub gain_h: String,
95}
96
97/// One Form 8949 part (Part I short-term on page 0, Part II long-term on page 1).
98#[derive(Debug, Clone, Deserialize)]
99pub struct PartMap {
100 /// `"short"` (Part I) or `"long"` (Part II).
101 pub term: String,
102 /// 0-based page index of this part within the bundled 2-page PDF.
103 pub page: usize,
104 /// The "not reported to the IRS" box checkbox field for this part's revision: the digital-asset
105 /// **Box I** (ST) / **Box L** (LT) on the 2025 map, and the securities **Box C** / **Box F** on
106 /// the pre-2025 (2024/2017) maps. Which one this is depends on the year the map was loaded for.
107 pub box_field: String,
108 /// The checkbox on-state (a PDF name without the leading `/`), e.g. `"6"`.
109 pub box_on: String,
110 /// The line-2 per-part totals row (d,e,g,h).
111 pub totals: AmountCols,
112 /// The 11 data rows; each row is the 8 column field names in order a,b,c,d,e,f,g,h.
113 pub rows: Vec<Vec<String>>,
114}
115
116/// The full Form 8949 field map for one tax year.
117#[derive(Debug, Clone, Deserialize)]
118pub struct Form8949Map {
119 /// `"f8949"`.
120 pub form: String,
121 /// Tax year (e.g. 2025).
122 pub year: i32,
123 /// "Name(s) shown on return" + SSN — on **both pages** (the 8949 is a two-page detail attachment, and
124 /// each page carries the header). `Option`: the crypto slice never writes it, and the 2017/2025 maps
125 /// have no verified FQNs. The FULL-return filler refuses on `None` — an unnamed 8949 is not filable
126 /// (Fable P6 r1 I3).
127 #[serde(default)]
128 pub identity_page1: Option<IdentityCells>,
129 #[serde(default)]
130 pub identity_page2: Option<IdentityCells>,
131 /// Rows per part per page — **map data**, not a hard-coded constant (a new form revision that
132 /// changes the grid is a data-only edit).
133 pub rows_per_page: usize,
134 /// The data-grid subform token used to re-derive the geometry bands — **per-year map config**,
135 /// not a const (2024 = `Table_Line1`, 2025 = `Table_Line1_Part`; the row fqns differ by year).
136 pub table_token: String,
137 /// Part I then Part II.
138 pub parts: Vec<PartMap>,
139}
140
141impl Form8949Map {
142 /// Parse the committed TOML.
143 pub fn parse(toml_src: &str) -> Result<Self, toml::de::Error> {
144 toml::from_str(toml_src)
145 }
146
147 /// The TY2025 map.
148 pub fn ty2025() -> Self {
149 Self::parse(F8949_MAP_2025).expect("bundled f8949 2025 map parses")
150 }
151
152 /// The TY2024 map.
153 pub fn ty2024() -> Self {
154 Self::parse(F8949_MAP_2024).expect("bundled f8949 2024 map parses")
155 }
156
157 /// The TY2017 map (pre-1099-DA: Box C/F, `/3`; field-identical grid to 2024).
158 pub fn ty2017() -> Self {
159 Self::parse(F8949_MAP_2017).expect("bundled f8949 2017 map parses")
160 }
161
162 /// The map for a supported tax year.
163 pub fn for_year(year: i32) -> Result<Self, FormsError> {
164 match year {
165 2017 => Ok(Self::ty2017()),
166 2024 => Ok(Self::ty2024()),
167 2025 => Ok(Self::ty2025()),
168 _ => Err(FormsError::UnsupportedYear(year)),
169 }
170 }
171
172 /// The part with the given term, if present.
173 pub fn part(&self, term: &str) -> Option<&PartMap> {
174 self.parts.iter().find(|p| p.term == term)
175 }
176}
177
178/// A checkbox choice (field + on-state) — used for the Schedule D QOF Yes/No answer and the Form 1040
179/// Digital-Asset Yes/No question.
180#[derive(Debug, Clone, Deserialize)]
181pub struct CheckChoice {
182 /// The checkbox field name.
183 pub field: String,
184 /// On-state PDF name (without leading `/`).
185 pub on: String,
186}
187
188/// A dollars-field + cents-field PAIR (the 2017 Schedule SE / Form 1040 / Form 8283 split every money
189/// amount into a whole-dollars field and a 2-digit cents field). The geometric oracle treats the pair
190/// as ONE logical cell **at the dollars-field geometry** (the cents field rides along as an authorized
191/// but geometry-exempt write). Because both fields descend from the same AcroForm root, `merge_copies`
192/// (which renames only the root `/T`) rewrites BOTH names as a unit — so overflow is safe.
193#[derive(Debug, Clone, Deserialize)]
194pub struct MoneyPair {
195 /// The whole-dollars field (the one the geometry oracle checks — column-x + row/descent).
196 pub dollars_field: String,
197 /// The 2-digit cents field (an authorized write; NOT independently geometry-checked).
198 pub cents_field: String,
199}
200
201/// A monetary cell: a single field carrying the whole formatted amount (2024/2025), or a
202/// dollars+cents [`MoneyPair`] (the 2017 forms). Deserializes untagged: a TOML **string** →
203/// [`MoneyCell::Single`]; a TOML **inline table** `{ dollars_field, cents_field }` →
204/// [`MoneyCell::Pair`].
205#[derive(Debug, Clone, Deserialize)]
206#[serde(untagged)]
207pub enum MoneyCell {
208 /// A single field holding the whole formatted amount.
209 Single(String),
210 /// A dollars-field + cents-field pair.
211 Pair(MoneyPair),
212}
213
214impl MoneyCell {
215 /// Every PDF field this cell targets (1 for a single, 2 for a pair) — for coverage guards.
216 pub fn fields(&self) -> Vec<&str> {
217 match self {
218 MoneyCell::Single(f) => vec![f.as_str()],
219 MoneyCell::Pair(p) => vec![p.dollars_field.as_str(), p.cents_field.as_str()],
220 }
221 }
222}
223
224/// A per-year default: the Digital-Asset question is present unless a year's map says otherwise.
225fn default_da_present() -> bool {
226 true
227}
228
229/// The Form 1040 capital-gains field map for one tax year: the capital-gain amount cell (line 7a in
230/// 2025 / line 7 in 2024 / **line 13** in 2017) + the Digital-Asset question (absent in 2017).
231/// The Form 1040's identity block (P6.2) — dumped and correlated against the printed form, never
232/// extrapolated. The SSN cells here declare `/MaxLen 9` (comb), so they take the nine BARE digits,
233/// while every schedule's SSN cell is `/MaxLen 11` and takes the hyphenated form. `push_identity`
234/// reads each cell's capacity rather than assuming either.
235#[derive(Debug, Clone, Deserialize)]
236pub struct Form1040HeaderCells {
237 pub taxpayer_first: String,
238 pub taxpayer_last: String,
239 pub taxpayer_ssn: String,
240 pub spouse_first: String,
241 pub spouse_last: String,
242 pub spouse_ssn: String,
243 pub address_street: String,
244 pub address_apt: String,
245 pub address_city: String,
246 pub address_state: String,
247 pub address_zip: String,
248 /// "If you checked the MFS box, enter the name of your spouse" — written on MFS only.
249 pub mfs_spouse_name: String,
250 /// The signature block's occupation cells (page 2).
251 pub occupation_taxpayer: String,
252 pub occupation_spouse: String,
253 /// The taxpayer's Identity Protection PIN cell (page 2, a 6-character comb). A paper return that
254 /// omits an ISSUED IP PIN is rejected or delayed (ARCH-P6.3a Q7 item 5).
255 pub ip_pin: String,
256 /// The §6096 Presidential Election Campaign boxes.
257 pub presidential_taxpayer: CheckChoice,
258 pub presidential_spouse: CheckChoice,
259 /// "Someone can claim: You / Your spouse as a dependent" — the §63(c)(5) floor's own checkbox.
260 pub claimed_dependent_taxpayer: CheckChoice,
261 pub claimed_dependent_spouse: CheckChoice,
262 /// "Spouse itemizes on a separate return or you were a dual-status alien" — §63(c)(6).
263 pub mfs_spouse_itemizes: CheckChoice,
264 /// ★ The four §63(f) aged/blind boxes. The IRS validates a nonstandard standard deduction by
265 /// COUNTING these, so L12 and this checkbox count must agree or the return fails its own
266 /// arithmetic cross-check (`p6-aged-blind-checkboxes-missing`).
267 pub taxpayer_aged: CheckChoice,
268 pub taxpayer_blind: CheckChoice,
269 pub spouse_aged: CheckChoice,
270 pub spouse_blind: CheckChoice,
271 /// "If more than four dependents, see instructions and check here" — v1 REFUSES instead (the
272 /// continuation statement is a synthetic page generator we do not have; same posture as Schedule
273 /// B's >14-payer refusal, SPEC §7.4 as amended). Mapped so the refusal can name the cell it will
274 /// not fill.
275 pub more_than_four_dependents: CheckChoice,
276 /// The four dependents rows the form physically has.
277 pub dependent_rows: Vec<DependentRowCells>,
278}
279
280/// One row of the 1040's dependents table. The name is a SINGLE cell spanning the printed
281/// "(1) First name / Last name" columns — the form has one widget there, not two.
282#[derive(Debug, Clone, Deserialize)]
283pub struct DependentRowCells {
284 pub name: String,
285 pub ssn: String,
286 pub relationship: String,
287 /// The Child-Tax-Credit box. NEVER checked: v1 omits CTC/ODC entirely (1040 L19 = 0, with the
288 /// `CtcOdcOmitted` advisory), and a checked credit box beside a zero credit is a form
289 /// contradicting itself. Mapped so the no-unmapped oracle knows the cell exists and is DELIBERATELY
290 /// left blank.
291 pub ctc: CheckChoice,
292 /// The Credit-for-Other-Dependents box. Never checked, same reason.
293 pub odc: CheckChoice,
294}
295
296#[derive(Debug, Clone, Deserialize)]
297pub struct Form1040Map {
298 /// `"f1040"`.
299 pub form: String,
300 /// Tax year.
301 pub year: i32,
302 /// The full-return identity BLOCK (P6.2). The 1040's header is not two cells like a schedule's: it
303 /// is names + SSNs + address + the §63(f) aged/blind checkboxes + the dependents table. `Option`
304 /// because this map is SHARED with the crypto slice, whose 2017/2025 editions have no verified
305 /// header FQNs; the FULL-return filler refuses on `None` rather than emit an unnamed 1040.
306 #[serde(default)]
307 pub header: Option<Form1040HeaderCells>,
308 /// The capital-gain amount cell (line 7a for 2025, line 7 for 2024, **line 13 for 2017**). A
309 /// single field on 2024/2025; a dollars+cents [`MoneyPair`] on the 2017 form.
310 pub line7a: MoneyCell,
311 /// Whether this year's 1040 carries the Digital-Asset question — **per-year scaffolding**. When
312 /// `true` (2024/2025) the fill answers it "Yes" and runs the map-independent adjacency guard;
313 /// **2017 sets it `false`** (no DA question — the map omits `da_yes`/`da_no` and the fill produces
314 /// the 1040 iff there is reportable capital activity).
315 #[serde(default = "default_da_present")]
316 pub da_present: bool,
317 /// Digital-Asset question "Yes" (LEFT member of the adjacent pair, on-state `/1`). `None` when the
318 /// year's 1040 has no DA question (2017).
319 #[serde(default)]
320 pub da_yes: Option<CheckChoice>,
321 /// Digital-Asset question "No" (right member, on-state `/2`) — never checked by btctax. `None`
322 /// when the year's 1040 has no DA question (2017).
323 #[serde(default)]
324 pub da_no: Option<CheckChoice>,
325
326 // ── Full-return extension (P6). Absent from the 2017/2025 maps, hence optional. ───────────
327 /// L1a — Σ W-2 box 1. AMOUNT column. Full-return only.
328 #[serde(default)]
329 pub line1a: Option<MoneyCell>,
330 /// L2a — tax-exempt interest. SUBLINE column. Full-return only (absent from the 2017/2025 maps).
331 #[serde(default)]
332 pub line2a: Option<MoneyCell>,
333 /// L1z — wages. AMOUNT column.
334 #[serde(default)]
335 pub line1z: Option<MoneyCell>,
336 /// L2b — taxable interest. AMOUNT column.
337 #[serde(default)]
338 pub line2b: Option<MoneyCell>,
339 /// L3a — qualified dividends. **SUBLINE column** (x ≈ [252,324]), not MID or AMOUNT.
340 #[serde(default)]
341 pub line3a: Option<MoneyCell>,
342 /// L3b — ordinary dividends. AMOUNT column.
343 #[serde(default)]
344 pub line3b: Option<MoneyCell>,
345 /// L8 — Schedule 1's printed L10.
346 #[serde(default)]
347 pub line8: Option<MoneyCell>,
348 /// L9 — total income.
349 #[serde(default)]
350 pub line9: Option<MoneyCell>,
351 /// L10 — Schedule 1's printed L26.
352 #[serde(default)]
353 pub line10: Option<MoneyCell>,
354 /// L11 — AGI.
355 #[serde(default)]
356 pub line11: Option<MoneyCell>,
357 /// L12 — the deduction claimed. **★ `f1_57` on the 2024 form is L12; on the 2025 form the same
358 /// field name is L1z** (SPEC §7.4). Per-(form, year) maps exist for exactly this.
359 #[serde(default)]
360 pub line12: Option<MoneyCell>,
361 /// L13 — Form 8995's printed L15 (QBI).
362 #[serde(default)]
363 pub line13: Option<MoneyCell>,
364 /// L14 — 12 + 13.
365 #[serde(default)]
366 pub line14: Option<MoneyCell>,
367 /// L15 — taxable income.
368 #[serde(default)]
369 pub line15: Option<MoneyCell>,
370 /// L16 — tax.
371 #[serde(default)]
372 pub line16: Option<MoneyCell>,
373 /// L17 — Schedule 2's printed L3 (always 0 in v1).
374 #[serde(default)]
375 pub line17: Option<MoneyCell>,
376 /// L18 — 16 + 17.
377 #[serde(default)]
378 pub line18: Option<MoneyCell>,
379 /// L19 — CTC/ODC (always 0 — a §3.4 conservative omission).
380 #[serde(default)]
381 pub line19: Option<MoneyCell>,
382 /// L20 — Schedule 3's printed L8.
383 #[serde(default)]
384 pub line20: Option<MoneyCell>,
385 /// L21 — 19 + 20.
386 #[serde(default)]
387 pub line21: Option<MoneyCell>,
388 /// L22 — 18 − 21.
389 #[serde(default)]
390 pub line22: Option<MoneyCell>,
391 /// L23 — Schedule 2's printed L21.
392 #[serde(default)]
393 pub line23: Option<MoneyCell>,
394 /// L24 — TOTAL TAX.
395 #[serde(default)]
396 pub line24: Option<MoneyCell>,
397 /// L25a — W-2 withholding. MID column.
398 #[serde(default)]
399 pub line25a: Option<MoneyCell>,
400 /// L25b — 1099 withholding. MID column.
401 #[serde(default)]
402 pub line25b: Option<MoneyCell>,
403 /// L25c — other withholding (Form 8959's printed L24). MID column.
404 #[serde(default)]
405 pub line25c: Option<MoneyCell>,
406 /// L25d — 25a + 25b + 25c.
407 #[serde(default)]
408 pub line25d: Option<MoneyCell>,
409 /// L26 — estimated tax payments.
410 #[serde(default)]
411 pub line26: Option<MoneyCell>,
412 /// L31 — Schedule 3's printed L15. MID column.
413 #[serde(default)]
414 pub line31: Option<MoneyCell>,
415 /// L32 — total other payments.
416 #[serde(default)]
417 pub line32: Option<MoneyCell>,
418 /// L33 — TOTAL PAYMENTS.
419 #[serde(default)]
420 pub line33: Option<MoneyCell>,
421 /// L34 — overpayment.
422 #[serde(default)]
423 pub line34: Option<MoneyCell>,
424 /// L35a — refunded to you.
425 #[serde(default)]
426 pub line35a: Option<MoneyCell>,
427 /// L37 — amount you owe.
428 #[serde(default)]
429 pub line37: Option<MoneyCell>,
430 /// The 5-way filing-status checkbox group.
431 #[serde(default)]
432 pub filing_status: Option<FilingStatusBoxes>,
433}
434
435/// The 1040's **5-way filing-status checkbox group**.
436///
437/// **★ The leaf field names COLLIDE.** Two distinct fields are both called `c1_3[0]` and two are both
438/// called `c1_3[1]`, distinguished only by their parent subform:
439///
440/// | status | fully-qualified name | on-state |
441/// |---|---|---|
442/// | Single | `…FilingStatus_ReadOrder[0].c1_3[0]` | `1` |
443/// | HoH | `…Page1[0].c1_3[0]` (no wrapper!) | `2` |
444/// | MFJ | `…FilingStatus_ReadOrder[0].c1_3[1]` | `3` |
445/// | MFS | `…FilingStatus_ReadOrder[0].c1_3[2]` | `4` |
446/// | QSS | `…Page1[0].c1_3[1]` (no wrapper!) | `5` |
447///
448/// A map keyed on the leaf name would silently check the WRONG FILING STATUS — which changes the
449/// standard deduction, every bracket, and every threshold on the return. The on-states are distinct
450/// and independently corroborate the mapping, so the filler asserts both.
451#[derive(Debug, Clone, Deserialize)]
452pub struct FilingStatusBoxes {
453 /// Single — on-state `1`.
454 pub single: CheckChoice,
455 /// Head of household — on-state `2`.
456 pub hoh: CheckChoice,
457 /// Married filing jointly — on-state `3`.
458 pub mfj: CheckChoice,
459 /// Married filing separately — on-state `4`.
460 pub mfs: CheckChoice,
461 /// Qualifying surviving spouse — on-state `5`.
462 pub qss: CheckChoice,
463}
464
465impl Form1040Map {
466 /// Parse the committed TOML.
467 pub fn parse(toml_src: &str) -> Result<Self, toml::de::Error> {
468 toml::from_str(toml_src)
469 }
470
471 /// The TY2025 map.
472 pub fn ty2025() -> Self {
473 Self::parse(F1040_MAP_2025).expect("bundled f1040 2025 map parses")
474 }
475
476 /// The TY2024 map.
477 pub fn ty2024() -> Self {
478 Self::parse(F1040_MAP_2024).expect("bundled f1040 2024 map parses")
479 }
480
481 /// The TY2017 map (capital gain on line 13; NO Digital-Asset question).
482 pub fn ty2017() -> Self {
483 Self::parse(F1040_MAP_2017).expect("bundled f1040 2017 map parses")
484 }
485
486 /// The map for a supported tax year.
487 pub fn for_year(year: i32) -> Result<Self, FormsError> {
488 match year {
489 2017 => Ok(Self::ty2017()),
490 2024 => Ok(Self::ty2024()),
491 2025 => Ok(Self::ty2025()),
492 _ => Err(FormsError::UnsupportedYear(year)),
493 }
494 }
495}
496
497/// One Form 8283 **Section A** row (Donated Property of $5,000 or Less): the 8 filled columns.
498#[derive(Debug, Clone, Deserialize)]
499pub struct Section8283ARow {
500 /// (a) Name and address of the donee organization.
501 pub donee: String,
502 /// (c) Description and condition of donated property.
503 pub desc: String,
504 /// (d) Date of the contribution (full date).
505 pub date_contrib: String,
506 /// (e) Date acquired by donor (mo., yr.).
507 pub date_acq: String,
508 /// (f) How acquired by donor.
509 pub how: String,
510 /// (g) Donor's cost or adjusted basis (money — a [`MoneyPair`] on the 2017 Rev. 12-2014 form).
511 pub cost: MoneyCell,
512 /// (h) Fair market value (money — a [`MoneyPair`] on the 2017 form).
513 pub fmv: MoneyCell,
514 /// (i) Method used to determine the FMV.
515 pub method: String,
516}
517
518/// Form 8283 Section A (page 1, Line 1) — up to 4 rows A–D.
519#[derive(Debug, Clone, Deserialize)]
520pub struct Section8283A {
521 /// The 4 rows A–D.
522 pub rows: Vec<Section8283ARow>,
523}
524
525/// One Form 8283 **Section B Part I** row (Over $5,000): the filled columns.
526#[derive(Debug, Clone, Deserialize)]
527pub struct Section8283BRow {
528 /// (a) Description of donated property.
529 pub desc: String,
530 /// (c) Appraised fair market value (money — a [`MoneyPair`] on the 2017 Rev. 12-2014 form).
531 pub fmv: MoneyCell,
532 /// (d) Date acquired by donor (mo., yr.).
533 pub date_acq: String,
534 /// (e) How acquired by donor.
535 pub how: String,
536 /// (f) Donor's cost or adjusted basis (money — a [`MoneyPair`] on the 2017 form).
537 pub cost: MoneyCell,
538 /// (i)/(h) Amount claimed as a deduction (carrier row only; money — a [`MoneyPair`] on 2017).
539 pub deduction: MoneyCell,
540}
541
542/// Form 8283 Section B (page 1/2, over-$5,000 property + page 2 identity) — up to 3 rows (2024/2025)
543/// or 4 rows (2017 Rev. 12-2014, `Line5A`–`Line5D`).
544#[derive(Debug, Clone, Deserialize)]
545pub struct Section8283B {
546 /// The property-type checkbox MUST be checked for BTC: **"k Digital assets"** (on-state `/11`) on
547 /// the Rev. 12-2023/2025 forms; the Rev. 12-2014 form has no digital-asset box, so 2017 uses
548 /// **"j Other"** (on-state `/9`) plus [`Self::btc_property_note`].
549 pub k_digital_assets: CheckChoice,
550 /// 2017 only: since "j Other" gives no category, the digital-asset nature is identified by a
551 /// printed note **prepended to the first row's (a) description** (e.g. "Other property: digital
552 /// asset (virtual currency)"). `None` on 2024/2025 ("k Digital assets" is self-describing).
553 #[serde(default)]
554 pub btc_property_note: Option<String>,
555 /// Part IV/III appraiser name (page 2). `None` when the revision has no printed-name field (the
556 /// Rev. 12-2014 form: the appraiser identity is the handwritten signature, left blank).
557 #[serde(default)]
558 pub appraiser_name: Option<String>,
559 /// Appraiser business address (page 2).
560 pub appraiser_address: String,
561 /// Appraiser identifying number (TIN/PTIN, page 2).
562 pub appraiser_tin: String,
563 /// Donee organization name (page 2).
564 pub donee_name: String,
565 /// Donee EIN (page 2).
566 pub donee_ein: String,
567 /// Donee address (page 2).
568 pub donee_address: String,
569 /// The rows (3 on 2024/2025, 4 on 2017) — the row count also sets the per-copy overflow cap.
570 pub rows: Vec<Section8283BRow>,
571}
572
573/// The Form 8283 (Rev. 12-2025) field map for one tax year.
574#[derive(Debug, Clone, Deserialize)]
575pub struct Form8283Map {
576 /// `"f8283"`.
577 pub form: String,
578 /// Tax year.
579 pub year: i32,
580 /// The FILER's identity — "Name(s) shown on your income tax return" + identifying number. `Option`
581 /// because the crypto slice never writes it (its 8283 rides beside a return btctax did not produce)
582 /// and the 2017/2025 maps have no verified FQNs; the FULL-return filler refuses on `None`.
583 #[serde(default)]
584 pub identity: Option<IdentityCells>,
585 /// Section A (≤ $5,000).
586 pub section_a: Section8283A,
587 /// Section B (> $5,000).
588 pub section_b: Section8283B,
589}
590
591impl Form8283Map {
592 /// Parse the committed TOML.
593 pub fn parse(toml_src: &str) -> Result<Self, toml::de::Error> {
594 toml::from_str(toml_src)
595 }
596
597 /// The TY2025 map.
598 pub fn ty2025() -> Self {
599 Self::parse(F8283_MAP_2025).expect("bundled f8283 2025 map parses")
600 }
601
602 /// The TY2024 map (Form 8283 Rev. 12-2023).
603 pub fn ty2024() -> Self {
604 Self::parse(F8283_MAP_2024).expect("bundled f8283 2024 map parses")
605 }
606
607 /// The TY2017 map (Form 8283 Rev. 12-2014 — "j Other", no DA box, 5/4 rows, ¢-pairs).
608 pub fn ty2017() -> Self {
609 Self::parse(F8283_MAP_2017).expect("bundled f8283 2017 map parses")
610 }
611
612 /// The map for a supported tax year.
613 pub fn for_year(year: i32) -> Result<Self, FormsError> {
614 match year {
615 2017 => Ok(Self::ty2017()),
616 2024 => Ok(Self::ty2024()),
617 2025 => Ok(Self::ty2025()),
618 _ => Err(FormsError::UnsupportedYear(year)),
619 }
620 }
621
622 /// Every field name the map targets (for the `map_YYYY_matches_bundled_pdf_fieldset` guard).
623 pub fn field_names(&self) -> Vec<&str> {
624 let mut v = Vec::new();
625 for r in &self.section_a.rows {
626 v.extend([
627 r.donee.as_str(),
628 r.desc.as_str(),
629 r.date_contrib.as_str(),
630 r.date_acq.as_str(),
631 r.how.as_str(),
632 ]);
633 v.extend(r.cost.fields());
634 v.extend(r.fmv.fields());
635 v.push(r.method.as_str());
636 }
637 let b = &self.section_b;
638 v.push(b.k_digital_assets.field.as_str());
639 if let Some(n) = &b.appraiser_name {
640 v.push(n.as_str());
641 }
642 v.extend([
643 b.appraiser_address.as_str(),
644 b.appraiser_tin.as_str(),
645 b.donee_name.as_str(),
646 b.donee_ein.as_str(),
647 b.donee_address.as_str(),
648 ]);
649 for r in &b.rows {
650 v.extend([r.desc.as_str(), r.date_acq.as_str(), r.how.as_str()]);
651 v.extend(r.fmv.fields());
652 v.extend(r.cost.fields());
653 v.extend(r.deduction.fields());
654 }
655 v
656 }
657}
658
659/// The Schedule D field map for one tax year.
660#[derive(Debug, Clone, Deserialize)]
661pub struct ScheduleDMap {
662 /// `"schedule_d"`.
663 pub form: String,
664 /// Tax year.
665 pub year: i32,
666 /// The name + SSN header cells (P6.2). `Option` because this map is SHARED with the crypto-slice
667 /// path, whose 2017/2025 editions have no verified identity FQNs and no `ReturnInputs` to source an
668 /// identity from. The FULL-return filler refuses on `None` — it may not emit an unnamed form.
669 #[serde(default)]
670 pub identity: Option<IdentityCells>,
671 /// Line 3 — Part I total from Form 8949 (Box C **or Box I**): columns d,e,g,h.
672 pub line3: AmountCols,
673 /// Line 7 — net short-term gain/loss (column h).
674 pub line7_h: String,
675 /// Line 10 — Part II total from Form 8949 (Box F **or Box L**): columns d,e,g,h.
676 pub line10: AmountCols,
677 /// Line 15 — net long-term gain/loss (column h).
678 pub line15_h: String,
679 /// Line 16 — total (line 7 + line 15), column h, page 2.
680 pub line16_h: String,
681 /// L6 — short-term capital loss carryover. **PAREN box ⇒ positive magnitude.** Full-return only
682 /// (`None` on the 2017/2025 maps, which serve the crypto-slice fill).
683 #[serde(default)]
684 pub line6: Option<MoneyCell>,
685 /// L13 — capital gain distributions (Σ 1099-DIV box 2a). Full-return only.
686 #[serde(default)]
687 pub line13: Option<MoneyCell>,
688 /// L14 — long-term capital loss carryover. **PAREN box ⇒ positive magnitude.** Full-return only.
689 #[serde(default)]
690 pub line14: Option<MoneyCell>,
691 /// L18 — 28%-Rate Gain Worksheet (always 0; a nonzero amount is refused upstream). Full-return only.
692 #[serde(default)]
693 pub line18: Option<MoneyCell>,
694 /// L19 — Unrecaptured §1250 Gain Worksheet (always 0; refused upstream). Full-return only.
695 #[serde(default)]
696 pub line19: Option<MoneyCell>,
697 /// L21 — the §1211(b) allowed loss offset. **PAREN box ⇒ positive magnitude.** Full-return only.
698 #[serde(default)]
699 pub line21: Option<MoneyCell>,
700 /// L17 — "Are lines 15 and 16 both gains?" Full-return only.
701 #[serde(default)]
702 pub line17: Option<YesNoPair>,
703 /// L20 — "Are lines 18 and 19 both zero or blank…?" Full-return only.
704 #[serde(default)]
705 pub line20: Option<YesNoPair>,
706 /// L22 — "Do you have qualified dividends on Form 1040, line 3a?" Full-return only.
707 #[serde(default)]
708 pub line22: Option<YesNoPair>,
709 /// The Part I amount-column subform token used to re-derive the geometry bands — **per-year map
710 /// config** (`Table_PartI` for 2024/2025, **`TablePartI`** (no underscore) for the 2017 form).
711 #[serde(default = "default_sched_d_token")]
712 pub table_token: String,
713 /// QOF question "Yes" choice. `None` on years whose Schedule D has no QOF question (2017 —
714 /// Qualified Opportunity Funds began in 2019).
715 #[serde(default)]
716 pub qof_yes: Option<CheckChoice>,
717 /// QOF question "No" choice (answered No when present). `None` on 2017 (no QOF question).
718 #[serde(default)]
719 pub qof_no: Option<CheckChoice>,
720}
721
722/// The default Schedule D Part I grid token (2024/2025); the 2017 map overrides it to `TablePartI`.
723fn default_sched_d_token() -> String {
724 "Table_PartI".to_string()
725}
726
727impl ScheduleDMap {
728 /// Parse the committed TOML.
729 pub fn parse(toml_src: &str) -> Result<Self, toml::de::Error> {
730 toml::from_str(toml_src)
731 }
732
733 /// The TY2025 map.
734 pub fn ty2025() -> Self {
735 Self::parse(SCHEDULE_D_MAP_2025).expect("bundled schedule_d 2025 map parses")
736 }
737
738 /// The TY2024 map.
739 pub fn ty2024() -> Self {
740 Self::parse(SCHEDULE_D_MAP_2024).expect("bundled schedule_d 2024 map parses")
741 }
742
743 /// The TY2017 map (grid token `TablePartI`; NO QOF question).
744 pub fn ty2017() -> Self {
745 Self::parse(SCHEDULE_D_MAP_2017).expect("bundled schedule_d 2017 map parses")
746 }
747
748 /// The map for a supported tax year.
749 pub fn for_year(year: i32) -> Result<Self, FormsError> {
750 match year {
751 2017 => Ok(Self::ty2017()),
752 2024 => Ok(Self::ty2024()),
753 2025 => Ok(Self::ty2025()),
754 _ => Err(FormsError::UnsupportedYear(year)),
755 }
756 }
757}
758
759/// The Form 8959 (Additional Medicare Tax) field map for one tax year.
760///
761/// Only the lines we FILL are mapped. Lines 2/3 (Form 4137 / Form 8919) and all of Part III plus
762/// line 23 (RRTA) are unmodeled and are deliberately absent — they stay blank on the filed form,
763/// which is why line 4 = line 1, line 18 = 7 + 13, and line 24 = line 22.
764#[derive(Debug, Clone, Deserialize)]
765pub struct Form8959Map {
766 /// `"f8959"`.
767 pub form: String,
768 /// Tax year.
769 pub year: i32,
770 /// The name + SSN header cells (P6.2). REQUIRED: a full-return schedule that does not name its
771 /// taxpayer is not a filable form, so a map lacking `[identity]` fails at deserialization.
772 pub identity: IdentityCells,
773 /// L1 — Σ W-2 box 5 Medicare wages, MID column.
774 pub line1: MoneyCell,
775 /// L4 — add lines 1–3 (2/3 blank ⇒ = line 1), MID column.
776 pub line4: MoneyCell,
777 /// L5 — filing-status threshold, MID column.
778 pub line5: MoneyCell,
779 /// L6 — line 4 − line 5, floored at 0, AMOUNT column.
780 pub line6: MoneyCell,
781 /// L7 — 0.9% × line 6, AMOUNT column.
782 pub line7: MoneyCell,
783 /// L8 — Schedule SE Part I line 6 (net SE earnings), MID column.
784 pub line8: MoneyCell,
785 /// L9 — filing-status threshold (again), MID column.
786 pub line9: MoneyCell,
787 /// L10 — the amount from line 4, MID column.
788 pub line10: MoneyCell,
789 /// L11 — line 9 − line 10, floored at 0, MID column.
790 pub line11: MoneyCell,
791 /// L12 — line 8 − line 11, floored at 0, AMOUNT column.
792 pub line12: MoneyCell,
793 /// L13 — 0.9% × line 12, AMOUNT column.
794 pub line13: MoneyCell,
795 /// L18 — add 7, 13, 17 → Schedule 2 line 11, AMOUNT column.
796 pub line18: MoneyCell,
797 /// L19 — Σ W-2 box 6 Medicare tax withheld, MID column.
798 pub line19: MoneyCell,
799 /// L20 — the amount from line 1, MID column.
800 pub line20: MoneyCell,
801 /// L21 — 1.45% × line 20, MID column.
802 pub line21: MoneyCell,
803 /// L22 — line 19 − line 21, floored at 0, AMOUNT column.
804 pub line22: MoneyCell,
805 /// L24 — add 22 and 23 → 1040 line 25c, AMOUNT column.
806 pub line24: MoneyCell,
807}
808
809impl Form8959Map {
810 /// Parse the committed TOML.
811 pub fn parse(toml_src: &str) -> Result<Self, toml::de::Error> {
812 toml::from_str(toml_src)
813 }
814
815 /// The TY2024 map.
816 pub fn ty2024() -> Self {
817 Self::parse(F8959_MAP_2024).expect("bundled f8959 2024 map parses")
818 }
819
820 /// The map for a supported tax year. Full-return v1 is **TY2024-only**: Form 8959 is reachable
821 /// only from the absolute return, which itself has tables for 2024 alone.
822 pub fn for_year(year: i32) -> Result<Self, FormsError> {
823 match year {
824 2024 => Ok(Self::ty2024()),
825 _ => Err(FormsError::UnsupportedYear(year)),
826 }
827 }
828
829 /// The 17 filled cells, in **printed reading order** (strictly descending y on page 1) — the
830 /// order `fill_form_8959` walks and the ordinal the geometric verifier checks the descent of.
831 pub fn lines(&self) -> [&MoneyCell; 17] {
832 [
833 &self.line1,
834 &self.line4,
835 &self.line5,
836 &self.line6,
837 &self.line7,
838 &self.line8,
839 &self.line9,
840 &self.line10,
841 &self.line11,
842 &self.line12,
843 &self.line13,
844 &self.line18,
845 &self.line19,
846 &self.line20,
847 &self.line21,
848 &self.line22,
849 &self.line24,
850 ]
851 }
852}
853
854/// The Form 8960 (Net Investment Income Tax) field map for one tax year.
855///
856/// Only the lines v1 FILLS are mapped. Annuities (3), Schedule E (4a–4c), CFC/PFIC (6), investment
857/// expenses (9a–9c, 10) and the whole estates-and-trusts branch (18a–21) are unmodeled and stay
858/// BLANK. The derived totals 9d and 11 ARE filled at zero — the form's arithmetic adds them.
859#[derive(Debug, Clone, Deserialize)]
860pub struct Form8960Map {
861 /// `"f8960"`.
862 pub form: String,
863 /// Tax year.
864 pub year: i32,
865 /// The name + SSN header cells (P6.2). REQUIRED: a full-return schedule that does not name its
866 /// taxpayer is not a filable form, so a map lacking `[identity]` fails at deserialization.
867 pub identity: IdentityCells,
868 /// L1 — taxable interest, AMOUNT column.
869 pub line1: MoneyCell,
870 /// L2 — ordinary dividends, AMOUNT column.
871 pub line2: MoneyCell,
872 /// L5a — net gain/loss from disposition of property, MID column.
873 pub line5a: MoneyCell,
874 /// L5d — combine 5a–5c, AMOUNT column.
875 pub line5d: MoneyCell,
876 /// L7 — other modifications, AMOUNT column.
877 pub line7: MoneyCell,
878 /// L8 — total investment income, AMOUNT column.
879 pub line8: MoneyCell,
880 /// L9d — add 9a/9b/9c (zero in v1), AMOUNT column.
881 pub line9d: MoneyCell,
882 /// L11 — total deductions and modifications (zero in v1), AMOUNT column.
883 pub line11: MoneyCell,
884 /// L12 — net investment income, AMOUNT column.
885 pub line12: MoneyCell,
886 /// L13 — modified AGI, MID column.
887 pub line13: MoneyCell,
888 /// L14 — the §1411(b) threshold (fillable, NOT pre-printed), MID column.
889 pub line14: MoneyCell,
890 /// L15 — 13 − 14, floored, MID column.
891 pub line15: MoneyCell,
892 /// L16 — smaller of 12 or 15, AMOUNT column.
893 pub line16: MoneyCell,
894 /// L17 — 3.8% × 16 → Schedule 2 line 12, AMOUNT column.
895 pub line17: MoneyCell,
896}
897
898impl Form8960Map {
899 /// Parse the committed TOML.
900 pub fn parse(toml_src: &str) -> Result<Self, toml::de::Error> {
901 toml::from_str(toml_src)
902 }
903 /// The TY2024 map.
904 pub fn ty2024() -> Self {
905 Self::parse(F8960_MAP_2024).expect("bundled f8960 2024 map parses")
906 }
907 /// The map for a supported tax year. Full-return v1 is TY2024-only.
908 pub fn for_year(year: i32) -> Result<Self, FormsError> {
909 match year {
910 2024 => Ok(Self::ty2024()),
911 _ => Err(FormsError::UnsupportedYear(year)),
912 }
913 }
914 /// The 14 filled cells in printed reading order (strictly descending y on page 1).
915 pub fn lines(&self) -> [&MoneyCell; 14] {
916 [
917 &self.line1,
918 &self.line2,
919 &self.line5a,
920 &self.line5d,
921 &self.line7,
922 &self.line8,
923 &self.line9d,
924 &self.line11,
925 &self.line12,
926 &self.line13,
927 &self.line14,
928 &self.line15,
929 &self.line16,
930 &self.line17,
931 ]
932 }
933}
934
935/// The Form 8995 (QBI deduction, simplified) field map for one tax year.
936///
937/// The Part I trade/business table (rows 1i–1v) and line 3 are deliberately unmapped: v1's only QBI
938/// is §199A REIT dividends, so there is no business to list. Lines 2/4/5 ARE filled, at zero.
939///
940/// **Lines 7, 16 and 17 are PARENTHESIZED boxes — the form prints the minus sign, so the value must
941/// be a POSITIVE MAGNITUDE.** `qbi::Form8995Lines` guarantees that.
942#[derive(Debug, Clone, Deserialize)]
943pub struct Form8995Map {
944 /// `"f8995"`.
945 pub form: String,
946 /// Tax year.
947 pub year: i32,
948 /// The name + SSN header cells (P6.2). REQUIRED: a full-return schedule that does not name its
949 /// taxpayer is not a filable form, so a map lacking `[identity]` fails at deserialization.
950 pub identity: IdentityCells,
951 /// Part I row 1i(a) — the trade or business's description.
952 pub row1_business: MoneyCell,
953 /// Part I row 1i(b) — its TIN (the filer's SSN; `/MaxLen` 11 ⇒ hyphenated).
954 pub row1_tin: MoneyCell,
955 /// Part I row 1i(c) — its QBI. With one business this IS line 2, which the form totals from it.
956 pub row1_qbi: MoneyCell,
957 /// L2 — total QBI: "Combine lines 1i through 1v, column (c)". MID column.
958 pub line2: MoneyCell,
959 /// L4 — combine 2 and 3, MID column.
960 pub line4: MoneyCell,
961 /// L5 — QBI component (20% × 4), AMOUNT column.
962 pub line5: MoneyCell,
963 /// L6 — qualified REIT dividends + PTP income, MID column.
964 pub line6: MoneyCell,
965 /// L7 — prior-year REIT/PTP loss carryforward, MID column. ★ positive magnitude (paren box).
966 pub line7: MoneyCell,
967 /// L8 — combine 6 and 7, MID column.
968 pub line8: MoneyCell,
969 /// L9 — REIT/PTP component (20% × 8), AMOUNT column.
970 pub line9: MoneyCell,
971 /// L10 — add 5 and 9, AMOUNT column.
972 pub line10: MoneyCell,
973 /// L11 — taxable income before the QBI deduction, MID column.
974 pub line11: MoneyCell,
975 /// L12 — net capital gain + qualified dividends, MID column.
976 pub line12: MoneyCell,
977 /// L13 — 11 − 12, floored, MID column.
978 pub line13: MoneyCell,
979 /// L14 — income limitation (20% × 13), AMOUNT column.
980 pub line14: MoneyCell,
981 /// L15 — the deduction: smaller of 10 or 14 → 1040 L13, AMOUNT column.
982 pub line15: MoneyCell,
983 /// L16 — total QB (loss) carryforward, AMOUNT column. ★ positive magnitude (paren box).
984 pub line16: MoneyCell,
985 /// L17 — total REIT/PTP (loss) carryforward, AMOUNT column. ★ positive magnitude (paren box).
986 pub line17: MoneyCell,
987}
988
989impl Form8995Map {
990 /// Parse the committed TOML.
991 pub fn parse(toml_src: &str) -> Result<Self, toml::de::Error> {
992 toml::from_str(toml_src)
993 }
994 /// The TY2024 map.
995 pub fn ty2024() -> Self {
996 Self::parse(F8995_MAP_2024).expect("bundled f8995 2024 map parses")
997 }
998 /// The map for a supported tax year. Full-return v1 is TY2024-only.
999 pub fn for_year(year: i32) -> Result<Self, FormsError> {
1000 match year {
1001 2024 => Ok(Self::ty2024()),
1002 _ => Err(FormsError::UnsupportedYear(year)),
1003 }
1004 }
1005 /// The 15 filled cells in printed reading order (strictly descending y on page 1).
1006 pub fn lines(&self) -> [&MoneyCell; 15] {
1007 [
1008 &self.line2,
1009 &self.line4,
1010 &self.line5,
1011 &self.line6,
1012 &self.line7,
1013 &self.line8,
1014 &self.line9,
1015 &self.line10,
1016 &self.line11,
1017 &self.line12,
1018 &self.line13,
1019 &self.line14,
1020 &self.line15,
1021 &self.line16,
1022 &self.line17,
1023 ]
1024 }
1025}
1026
1027/// The Schedule 2 (Additional Taxes) field map for one tax year.
1028///
1029/// Part I is entirely absent: line 1a (excess APTC) has no input and would refuse if it did, and
1030/// line 2 (AMT) is $0 by construction (the return is refused if the Form 6251 screen trips). Only
1031/// the three Part II taxes v1 computes are mapped. **Line 21 is on PAGE 2.**
1032#[derive(Debug, Clone, Deserialize)]
1033pub struct Schedule2Map {
1034 /// `"f1040s2"`.
1035 pub form: String,
1036 /// Tax year.
1037 pub year: i32,
1038 /// The name + SSN header cells (P6.2). REQUIRED: a full-return schedule that does not name its
1039 /// taxpayer is not a filable form, so a map lacking `[identity]` fails at deserialization.
1040 pub identity: IdentityCells,
1041 /// L4 — self-employment tax (SS + regular Medicare only), AMOUNT column, page 1.
1042 pub line4: MoneyCell,
1043 /// L11 — Additional Medicare Tax (Form 8959's printed L18), AMOUNT column, page 1.
1044 pub line11: MoneyCell,
1045 /// L12 — net investment income tax (Form 8960's printed L17), AMOUNT column, page 1.
1046 pub line12: MoneyCell,
1047 /// L21 — total other taxes → 1040 L23, AMOUNT column, **page 2**.
1048 pub line21: MoneyCell,
1049}
1050
1051impl Schedule2Map {
1052 /// Parse the committed TOML.
1053 pub fn parse(toml_src: &str) -> Result<Self, toml::de::Error> {
1054 toml::from_str(toml_src)
1055 }
1056 /// The TY2024 map.
1057 pub fn ty2024() -> Self {
1058 Self::parse(SCHEDULE_2_MAP_2024).expect("bundled schedule 2 2024 map parses")
1059 }
1060 /// The map for a supported tax year. Full-return v1 is TY2024-only.
1061 pub fn for_year(year: i32) -> Result<Self, FormsError> {
1062 match year {
1063 2024 => Ok(Self::ty2024()),
1064 _ => Err(FormsError::UnsupportedYear(year)),
1065 }
1066 }
1067 /// The 4 filled cells in printed reading order. **Descent is grouped by PAGE** — line 21 sits on
1068 /// page 2, whose y-coordinates are not comparable with page 1's.
1069 pub fn lines(&self) -> [&MoneyCell; 4] {
1070 [&self.line4, &self.line11, &self.line12, &self.line21]
1071 }
1072}
1073
1074/// The Schedule 3 (Additional Credits and Payments) field map for one tax year.
1075///
1076/// Only the foreign tax credit (L1) and the §6413(c) excess-Social-Security credit (L11) are mapped.
1077/// Every other Part I credit is a §3.4 conservative omission and stays BLANK.
1078#[derive(Debug, Clone, Deserialize)]
1079pub struct Schedule3Map {
1080 /// `"f1040s3"`.
1081 pub form: String,
1082 /// Tax year.
1083 pub year: i32,
1084 /// The name + SSN header cells (P6.2). REQUIRED: a full-return schedule that does not name its
1085 /// taxpayer is not a filable form, so a map lacking `[identity]` fails at deserialization.
1086 pub identity: IdentityCells,
1087 /// L1 — foreign tax credit, AMOUNT column.
1088 pub line1: MoneyCell,
1089 /// L8 — total nonrefundable credits → 1040 L20, AMOUNT column.
1090 pub line8: MoneyCell,
1091 /// L10 — "Amount paid with request for extension to file", AMOUNT column. ★ Its absence made the
1092 /// filed return demand a payment the filer had ALREADY made (Fable ARCH-P6.3a D1).
1093 pub line10: MoneyCell,
1094 /// L11 — excess Social Security / tier-1 RRTA withheld, AMOUNT column.
1095 pub line11: MoneyCell,
1096 /// L15 — total other payments → 1040 L31, AMOUNT column.
1097 pub line15: MoneyCell,
1098}
1099
1100impl Schedule3Map {
1101 /// Parse the committed TOML.
1102 pub fn parse(toml_src: &str) -> Result<Self, toml::de::Error> {
1103 toml::from_str(toml_src)
1104 }
1105 /// The TY2024 map.
1106 pub fn ty2024() -> Self {
1107 Self::parse(SCHEDULE_3_MAP_2024).expect("bundled schedule 3 2024 map parses")
1108 }
1109 /// The map for a supported tax year. Full-return v1 is TY2024-only.
1110 pub fn for_year(year: i32) -> Result<Self, FormsError> {
1111 match year {
1112 2024 => Ok(Self::ty2024()),
1113 _ => Err(FormsError::UnsupportedYear(year)),
1114 }
1115 }
1116 /// The 4 filled cells in printed reading order (strictly descending y on page 1).
1117 pub fn lines(&self) -> [&MoneyCell; 5] {
1118 [
1119 &self.line1,
1120 &self.line8,
1121 &self.line10,
1122 &self.line11,
1123 &self.line15,
1124 ]
1125 }
1126}
1127
1128/// The Schedule A (Itemized Deductions) field map for one tax year.
1129///
1130/// **Three x-clusters** — Schedule A is the only form here that needs a third. Line 2 (the AGI the
1131/// 7.5% medical floor is taken on) sits INLINE with the printed sentence at x ≈ [331,403], not in the
1132/// MID column, and it is the same WIDTH as MID, so nothing but its x-position distinguishes it.
1133///
1134/// Unmapped on purpose: line 6 (other taxes), 8b/8c (mortgage not on a 1098; points), 9 (investment
1135/// interest), 15 (casualty), 16 (other). **Line 8d is a ReadOnly "Reserved for future use" widget** —
1136/// live, and it consumes a suffix number. Never write it.
1137#[derive(Debug, Clone, Deserialize)]
1138pub struct ScheduleAMap {
1139 /// `"f1040sa"`.
1140 pub form: String,
1141 /// Tax year.
1142 pub year: i32,
1143 /// L5a's §164(b)(5) sales-tax election checkbox — the election core already honours in the
1144 /// arithmetic, which the filed form never showed (ARCH-P6.3a Q7 item 3).
1145 pub check_5a_sales_tax: CheckChoice,
1146 /// ★ §2.7 — L8's §163(h)(3)(F) mixed-use-mortgage checkbox: "If you didn't use all of your home
1147 /// mortgage loan(s) to buy, build, or improve your home, check this box." Nested under
1148 /// `Line8_ReadOrder[0]`, like line 18's own read-order box.
1149 pub check_8_mixed_use: CheckChoice,
1150 /// L18's §63(e) "itemize even though less than the standard deduction" checkbox (Q7 item 4).
1151 pub check_18_elects_smaller: CheckChoice,
1152 /// The name + SSN header cells (P6.2). REQUIRED: a full-return schedule that does not name its
1153 /// taxpayer is not a filable form, so a map lacking `[identity]` fails at deserialization.
1154 pub identity: IdentityCells,
1155 /// L1 — medical and dental expenses, MID column.
1156 pub line1: MoneyCell,
1157 /// L2 — AGI. ★ **AGI-INLINE column**, not MID.
1158 pub line2: MoneyCell,
1159 /// L3 — the §213(a) 7.5% floor, MID column.
1160 pub line3: MoneyCell,
1161 /// L4 — medical allowed, AMOUNT column.
1162 pub line4: MoneyCell,
1163 /// L5a — state/local income or sales taxes, MID column.
1164 pub line5a: MoneyCell,
1165 /// L5b — real-estate taxes, MID column.
1166 pub line5b: MoneyCell,
1167 /// L5c — personal-property taxes, MID column.
1168 pub line5c: MoneyCell,
1169 /// L5d — add 5a-5c, MID column.
1170 pub line5d: MoneyCell,
1171 /// L5e — the §164(b) SALT cap, MID column.
1172 pub line5e: MoneyCell,
1173 /// L7 — add 5e and 6, AMOUNT column.
1174 pub line7: MoneyCell,
1175 /// L8a — mortgage interest on Form 1098, MID column.
1176 pub line8a: MoneyCell,
1177 /// L8e — add 8a-8c, MID column.
1178 pub line8e: MoneyCell,
1179 /// L10 — add 8e and 9, AMOUNT column.
1180 pub line10: MoneyCell,
1181 /// L11 — gifts by cash or check, MID column.
1182 pub line11: MoneyCell,
1183 /// L12 — gifts other than cash (incl. crypto), MID column.
1184 pub line12: MoneyCell,
1185 /// L13 — prior-year carryover, MID column.
1186 pub line13: MoneyCell,
1187 /// L14 — add 11-13, AMOUNT column.
1188 pub line14: MoneyCell,
1189 /// L17 — total itemized deductions → 1040 L12, AMOUNT column.
1190 pub line17: MoneyCell,
1191}
1192
1193impl ScheduleAMap {
1194 /// Parse the committed TOML.
1195 pub fn parse(toml_src: &str) -> Result<Self, toml::de::Error> {
1196 toml::from_str(toml_src)
1197 }
1198 /// The TY2024 map.
1199 pub fn ty2024() -> Self {
1200 Self::parse(SCHEDULE_A_MAP_2024).expect("bundled schedule A 2024 map parses")
1201 }
1202 /// The map for a supported tax year. Full-return v1 is TY2024-only.
1203 pub fn for_year(year: i32) -> Result<Self, FormsError> {
1204 match year {
1205 2024 => Ok(Self::ty2024()),
1206 _ => Err(FormsError::UnsupportedYear(year)),
1207 }
1208 }
1209 /// The 18 filled cells in printed reading order (strictly descending y on page 1).
1210 pub fn lines(&self) -> [&MoneyCell; 18] {
1211 [
1212 &self.line1,
1213 &self.line2,
1214 &self.line3,
1215 &self.line4,
1216 &self.line5a,
1217 &self.line5b,
1218 &self.line5c,
1219 &self.line5d,
1220 &self.line5e,
1221 &self.line7,
1222 &self.line8a,
1223 &self.line8e,
1224 &self.line10,
1225 &self.line11,
1226 &self.line12,
1227 &self.line13,
1228 &self.line14,
1229 &self.line17,
1230 ]
1231 }
1232}
1233
1234/// The Schedule 1 (Additional Income and Adjustments to Income) field map for one tax year.
1235///
1236/// Root subform is `form1[0]` (as on Schedule 2), NOT `topmostSubform[0]`. **Two pages** — Part II is
1237/// entirely on page 2, so descent is grouped by page.
1238///
1239/// **Line 22 is a ReadOnly "Reserved for future use" widget** that consumes a suffix number; never
1240/// written. Non-money fields (a date on 2b, an SSN comb on 19b, a date on 19c) sit inside the money
1241/// x-band — writing a dollar amount into one prints garbage.
1242#[derive(Debug, Clone, Deserialize)]
1243pub struct Schedule1Map {
1244 /// `"f1040s1"`.
1245 pub form: String,
1246 /// Tax year.
1247 pub year: i32,
1248 /// The name + SSN header cells (P6.2). REQUIRED: a full-return schedule that does not name its
1249 /// taxpayer is not a filable form, so a map lacking `[identity]` fails at deserialization.
1250 pub identity: IdentityCells,
1251 /// L1 — taxable state/local refund, AMOUNT column, page 1.
1252 pub line1: MoneyCell,
1253 /// L3 — business income (crypto Schedule C net), AMOUNT column, page 1.
1254 pub line3: MoneyCell,
1255 /// L7 — unemployment compensation, AMOUNT column, page 1.
1256 pub line7: MoneyCell,
1257 /// L8v — digital assets received as ordinary income, **MID column**, page 1.
1258 pub line8v: MoneyCell,
1259 /// L9 — total other income, AMOUNT column, page 1.
1260 pub line9: MoneyCell,
1261 /// L10 — combine 1–7 and 9 → 1040 L8, AMOUNT column, page 1.
1262 pub line10: MoneyCell,
1263 /// L15 — deductible part of SE tax, AMOUNT column, **page 2**.
1264 pub line15: MoneyCell,
1265 /// L18 — early-withdrawal penalty, AMOUNT column, page 2.
1266 pub line18: MoneyCell,
1267 /// L21 — student-loan interest deduction, AMOUNT column, page 2.
1268 pub line21: MoneyCell,
1269 /// L26 — total adjustments → 1040 L10, AMOUNT column, page 2.
1270 pub line26: MoneyCell,
1271}
1272
1273impl Schedule1Map {
1274 /// Parse the committed TOML.
1275 pub fn parse(toml_src: &str) -> Result<Self, toml::de::Error> {
1276 toml::from_str(toml_src)
1277 }
1278 /// The TY2024 map.
1279 pub fn ty2024() -> Self {
1280 Self::parse(SCHEDULE_1_MAP_2024).expect("bundled schedule 1 2024 map parses")
1281 }
1282 /// The map for a supported tax year. Full-return v1 is TY2024-only.
1283 pub fn for_year(year: i32) -> Result<Self, FormsError> {
1284 match year {
1285 2024 => Ok(Self::ty2024()),
1286 _ => Err(FormsError::UnsupportedYear(year)),
1287 }
1288 }
1289 /// The 10 filled cells in printed reading order. **Descent is grouped by PAGE.**
1290 pub fn lines(&self) -> [&MoneyCell; 10] {
1291 [
1292 &self.line1,
1293 &self.line3,
1294 &self.line7,
1295 &self.line8v,
1296 &self.line9,
1297 &self.line10,
1298 &self.line15,
1299 &self.line18,
1300 &self.line21,
1301 &self.line26,
1302 ]
1303 }
1304}
1305
1306/// The Schedule C (Profit or Loss From Business) field map — the crypto trade or business.
1307///
1308/// **Its money column is x ≈ [475, 576]** — not the [504, 576] of Schedules 1/2/3/A and Forms
1309/// 8959/8960/8995, and not Schedule B's [489.6, 576]. No amount-column constant is shared between
1310/// forms in this crate, and none may be.
1311///
1312/// Part II's individual expense lines (8–27b) are unmapped: v1 takes a FLAT expense total, so only
1313/// line 28 is printed. Line 30 (home office) and the line-32 at-risk checkboxes are unmapped too — a
1314/// Schedule C loss refuses upstream, so line 31 is always ≥ 0.
1315#[derive(Debug, Clone, Deserialize)]
1316pub struct ScheduleCMap {
1317 /// `"f1040sc"`.
1318 pub form: String,
1319 /// Tax year.
1320 pub year: i32,
1321 /// Line A — "Principal business or profession".
1322 pub line_a_business: String,
1323 /// Line B — the NAICS code (a 6-character comb).
1324 pub line_b_naics: String,
1325 /// Line F — the accounting-method checkboxes. `(1) Cash` and `(2) Accrual`; `(3) Other` is never
1326 /// checked (v1 captures only the two).
1327 pub method_cash: CheckChoice,
1328 pub method_accrual: CheckChoice,
1329 /// The name + SSN header cells (P6.2). REQUIRED: a full-return schedule that does not name its
1330 /// taxpayer is not a filable form, so a map lacking `[identity]` fails at deserialization.
1331 pub identity: IdentityCells,
1332 /// L1 — gross receipts or sales.
1333 pub line1: MoneyCell,
1334 /// L3 — line 1 − line 2 (returns, blank).
1335 pub line3: MoneyCell,
1336 /// L5 — gross profit (line 3 − line 4, COGS blank).
1337 pub line5: MoneyCell,
1338 /// L7 — gross income (line 5 + line 6, other income blank).
1339 pub line7: MoneyCell,
1340 /// L28 — total expenses.
1341 pub line28: MoneyCell,
1342 /// L29 — tentative profit (line 7 − line 28).
1343 pub line29: MoneyCell,
1344 /// L31 — net profit → Schedule 1 L3 **and** Schedule SE L2.
1345 pub line31: MoneyCell,
1346}
1347
1348impl ScheduleCMap {
1349 /// Parse the committed TOML.
1350 pub fn parse(toml_src: &str) -> Result<Self, toml::de::Error> {
1351 toml::from_str(toml_src)
1352 }
1353 /// The TY2024 map.
1354 pub fn ty2024() -> Self {
1355 Self::parse(SCHEDULE_C_MAP_2024).expect("bundled schedule C 2024 map parses")
1356 }
1357 /// The map for a supported tax year. Full-return v1 is TY2024-only.
1358 pub fn for_year(year: i32) -> Result<Self, FormsError> {
1359 match year {
1360 2024 => Ok(Self::ty2024()),
1361 _ => Err(FormsError::UnsupportedYear(year)),
1362 }
1363 }
1364 /// The 7 filled cells in printed reading order (strictly descending y on page 1).
1365 pub fn lines(&self) -> [&MoneyCell; 7] {
1366 [
1367 &self.line1,
1368 &self.line3,
1369 &self.line5,
1370 &self.line7,
1371 &self.line28,
1372 &self.line29,
1373 &self.line31,
1374 ]
1375 }
1376}
1377
1378/// One listed-payer row on Schedule B: the payer-name text cell + the amount cell.
1379#[derive(Debug, Clone, Deserialize)]
1380pub struct ScheduleBRowMap {
1381 /// The payer-name field (a wide text cell in the PAYER column).
1382 pub payer: String,
1383 /// The amount field.
1384 pub amount: MoneyCell,
1385}
1386
1387/// A Yes/No checkbox pair (Schedule B Part III). Both boxes share the same on-states (`"1"`/`"2"`) and
1388/// the same x geometry across every pair on the form, so only the field NAME distinguishes them.
1389#[derive(Debug, Clone, Deserialize)]
1390pub struct YesNoPair {
1391 /// The "Yes" box.
1392 pub yes: CheckChoice,
1393 /// The "No" box.
1394 pub no: CheckChoice,
1395}
1396
1397/// The Schedule B (Interest and Ordinary Dividends) field map for one tax year.
1398///
1399/// **Its amount column is x ≈ [489.6, 576]** — not the [504, 576] of Schedules 1/2/3/A and Forms
1400/// 8959/8960/8995, nor Schedule C's [475, 576]. A shared constant would reject every cell.
1401///
1402/// **Row 1 of BOTH repeating tables has a different parent subform** (`Line1_ReadOrder` in Part I,
1403/// `ReadOrderControl` in Part II) while its amount sibling does not — so the rows are written out in
1404/// full in the TOML rather than interpolated. **Part I has 14 rows, Part II has 15**; the asymmetry
1405/// is real.
1406#[derive(Debug, Clone, Deserialize)]
1407pub struct ScheduleBMap {
1408 /// `"f1040sb"`.
1409 pub form: String,
1410 /// Tax year.
1411 pub year: i32,
1412 /// L7b — the foreign-country list. It IS a captured input; the claim that v1 had none was false
1413 /// (ARCH-P6.3a Q7 item 7).
1414 pub line7b_countries: String,
1415 /// The name + SSN header cells (P6.2). REQUIRED: a full-return schedule that does not name its
1416 /// taxpayer is not a filable form, so a map lacking `[identity]` fails at deserialization.
1417 pub identity: IdentityCells,
1418 /// Part I line 1 — the 14 interest-payer rows.
1419 pub part1_rows: Vec<ScheduleBRowMap>,
1420 /// L2 — add the amounts on line 1.
1421 pub line2: MoneyCell,
1422 /// L4 — line 2 − line 3 → 1040 L2b.
1423 pub line4: MoneyCell,
1424 /// Part II line 5 — the 15 dividend-payer rows.
1425 pub part2_rows: Vec<ScheduleBRowMap>,
1426 /// L6 — add the amounts on line 5 → 1040 L3b.
1427 pub line6: MoneyCell,
1428 /// L7a — the foreign-account Yes/No pair.
1429 pub line7a: YesNoPair,
1430 /// L8 — the foreign-trust Yes/No pair.
1431 pub line8: YesNoPair,
1432}
1433
1434impl ScheduleBMap {
1435 /// Parse the committed TOML.
1436 pub fn parse(toml_src: &str) -> Result<Self, toml::de::Error> {
1437 toml::from_str(toml_src)
1438 }
1439 /// The TY2024 map.
1440 pub fn ty2024() -> Self {
1441 Self::parse(SCHEDULE_B_MAP_2024).expect("bundled schedule B 2024 map parses")
1442 }
1443 /// The map for a supported tax year. Full-return v1 is TY2024-only.
1444 pub fn for_year(year: i32) -> Result<Self, FormsError> {
1445 match year {
1446 2024 => Ok(Self::ty2024()),
1447 _ => Err(FormsError::UnsupportedYear(year)),
1448 }
1449 }
1450}
1451
1452/// The Schedule SE (Form 1040) field map for one tax year — the filled §1401 line chain.
1453#[derive(Debug, Clone, Deserialize)]
1454pub struct ScheduleSeMap {
1455 /// `"schedule_se"`.
1456 pub form: String,
1457 /// Tax year.
1458 pub year: i32,
1459 /// The identity header — "Name of person **with self-employment income**" + THAT person's SSN, i.e.
1460 /// the PROPRIETOR, not the return's joint name line. `Option` because this map is shared with the
1461 /// crypto slice (whose 2017/2025 editions have no verified identity FQNs and write no identity at
1462 /// all); the FULL-return filler refuses on `None`.
1463 #[serde(default)]
1464 pub identity: Option<IdentityCells>,
1465 /// Line 2 — net profit (net_se), amount column.
1466 pub line2: MoneyCell,
1467 /// Line 3 — combine 1a/1b/2 (= line 2), amount column.
1468 pub line3: MoneyCell,
1469 /// Line 4a — net SE earnings (base = net_se × 92.35%), amount column.
1470 pub line4a: MoneyCell,
1471 /// Line 4c — combine 4a/4b (= line 4a), amount column. The $400 STOP threshold.
1472 pub line4c: MoneyCell,
1473 /// Line 6 — add 4c/5b (= line 4c), amount column.
1474 pub line6: MoneyCell,
1475 /// Line 8a — Form W-2 Social Security wages, **MID column**.
1476 pub line8a: MoneyCell,
1477 /// Line 8d — add 8a/8b/8c (= line 8a), amount column.
1478 pub line8d: MoneyCell,
1479 /// Line 9 — line 7 (`ss_wage_base` constant) − line 8d, amount column.
1480 pub line9: MoneyCell,
1481 /// Line 10 — Social Security portion (`ss`), amount column.
1482 pub line10: MoneyCell,
1483 /// Line 11 — regular Medicare portion (`medicare`), amount column.
1484 pub line11: MoneyCell,
1485 /// Line 12 — SE tax = line 10 + line 11 (**SS + regular Medicare ONLY**), amount column.
1486 pub line12: MoneyCell,
1487 /// Line 13 — one-half SE-tax deduction (= line 12 × 50% = `deductible_half`), **MID column**.
1488 pub line13: MoneyCell,
1489 /// Fields the BLANK form already carries a factory `/V` for (the 2017 §B long form pre-prints
1490 /// line 7 = `127,200`/`00` and line 14 = `5,200`/`00`) — excluded from the `no_unmapped_filled`
1491 /// guard so those constants don't read as stray writes. Empty on 2024/2025.
1492 #[serde(default)]
1493 pub prefilled_exempt: Vec<String>,
1494}
1495
1496impl ScheduleSeMap {
1497 /// Parse the committed TOML.
1498 pub fn parse(toml_src: &str) -> Result<Self, toml::de::Error> {
1499 toml::from_str(toml_src)
1500 }
1501
1502 /// The TY2025 map.
1503 pub fn ty2025() -> Self {
1504 Self::parse(SCHEDULE_SE_MAP_2025).expect("bundled schedule_se 2025 map parses")
1505 }
1506
1507 /// The TY2024 map (field-name-identical to 2025; only the wage base differs).
1508 pub fn ty2024() -> Self {
1509 Self::parse(SCHEDULE_SE_MAP_2024).expect("bundled schedule_se 2024 map parses")
1510 }
1511
1512 /// The TY2017 map (OLD §B long form: dollars+cents pairs; pre-filled line 7/14 exempt).
1513 pub fn ty2017() -> Self {
1514 Self::parse(SCHEDULE_SE_MAP_2017).expect("bundled schedule_se 2017 map parses")
1515 }
1516
1517 /// The map for a supported tax year.
1518 pub fn for_year(year: i32) -> Result<Self, FormsError> {
1519 match year {
1520 2017 => Ok(Self::ty2017()),
1521 2024 => Ok(Self::ty2024()),
1522 2025 => Ok(Self::ty2025()),
1523 _ => Err(FormsError::UnsupportedYear(year)),
1524 }
1525 }
1526
1527 /// The 12 filled line cells, in chain order.
1528 pub fn lines(&self) -> [&MoneyCell; 12] {
1529 [
1530 &self.line2,
1531 &self.line3,
1532 &self.line4a,
1533 &self.line4c,
1534 &self.line6,
1535 &self.line8a,
1536 &self.line8d,
1537 &self.line9,
1538 &self.line10,
1539 &self.line11,
1540 &self.line12,
1541 &self.line13,
1542 ]
1543 }
1544
1545 /// Every field name the map targets (for the `map_YYYY_matches_bundled_pdf_fieldset` guard) —
1546 /// both members of each dollars+cents pair on the 2017 form.
1547 pub fn field_names(&self) -> Vec<&str> {
1548 self.lines().iter().flat_map(|c| c.fields()).collect()
1549 }
1550}