1use crate::reference::Reference;
12use crate::values::{ComponentValues, ProcHints, ProcValues, RenderOptions};
13use citum_schema::locale::{GrammaticalGender, TermForm};
14use citum_schema::reference::ClassExtension;
15use citum_schema::template::{NumberVariable, TemplateNumber};
16
17fn resolve_number_value(
19 number: &NumberVariable,
20 reference: &Reference,
21 hints: &ProcHints,
22 options: &RenderOptions<'_>,
23 show_with_locator: bool,
24) -> Option<String> {
25 match number {
26 NumberVariable::Volume => reference.volume().map(|v| v.to_string()),
27 NumberVariable::Issue => reference.issue().map(|v| v.to_string()),
28 NumberVariable::Pages => {
29 let suppress = !show_with_locator
30 && options.context == crate::values::RenderContext::Citation
31 && options.locator_raw.is_some()
32 && matches!(
33 options.config.processing,
34 Some(citum_schema::options::Processing::Note)
35 );
36 if suppress {
37 None
38 } else {
39 reference.pages().map(|p| {
40 format_page_range(&p.to_string(), options.config.page_range_format.as_ref())
41 })
42 }
43 }
44 NumberVariable::ChapterNumber => match reference.extension() {
45 ClassExtension::Statute(r) => r.chapter_number.clone(),
46 _ => reference.numbering_value(&citum_schema::reference::NumberingType::Chapter),
47 },
48 NumberVariable::Edition => reference.edition(),
49 NumberVariable::CollectionNumber => reference.collection_number(),
50 NumberVariable::Number => reference.number(),
51 NumberVariable::Custom(kind) => reference.numbering_value(
52 &citum_schema::reference::NumberingType::Custom(kind.clone()),
53 ),
54 NumberVariable::DocketNumber => match reference.extension() {
55 ClassExtension::Brief(r) => r.docket_number.clone(),
56 _ => None,
57 },
58 NumberVariable::PatentNumber => match reference.extension() {
59 ClassExtension::Patent(r) => Some(r.patent_number.clone()),
60 _ => None,
61 },
62 NumberVariable::StandardNumber => match reference.extension() {
63 ClassExtension::Standard(r) => Some(r.standard_number.clone()),
64 _ => None,
65 },
66 NumberVariable::ReportNumber => reference.report_number(),
67 NumberVariable::PartNumber => {
68 reference.numbering_value(&citum_schema::reference::NumberingType::Part)
69 }
70 NumberVariable::SupplementNumber => {
71 reference.numbering_value(&citum_schema::reference::NumberingType::Supplement)
72 }
73 NumberVariable::PrintingNumber => {
74 reference.numbering_value(&citum_schema::reference::NumberingType::Printing)
75 }
76 NumberVariable::FirstReferenceNoteNumber => {
77 hints.first_reference_note_number.map(|n| n.to_string())
78 }
79 NumberVariable::CitationNumber => hints.citation_number.map(|n| {
80 if options.context == crate::values::RenderContext::Citation
81 && let Some(sub_label) = &hints.citation_sub_label
82 {
83 return format!("{n}{sub_label}");
84 }
85 n.to_string()
86 }),
87 NumberVariable::CitationLabel => {
88 let Some(citum_schema::options::Processing::Label(config)) =
89 options.config.processing.as_ref()
90 else {
91 return None;
92 };
93 let params = config.effective_params();
94 let base = crate::processor::labels::generate_base_label(reference, ¶ms);
95 if base.is_empty() {
96 return None;
97 }
98 let suffix = if hints.disamb_condition && hints.group_index > 0 {
99 crate::values::int_to_letter(hints.group_index as u32).unwrap_or_default()
100 } else {
101 String::new()
102 };
103 Some(format!("{base}{suffix}"))
104 }
105 _ => None,
106 }
107}
108
109fn resolve_number_label<F: crate::render::format::OutputFormat<Output = String>>(
111 number: &NumberVariable,
112 label_form: &citum_schema::template::LabelForm,
113 value: &str,
114 requested_gender: Option<GrammaticalGender>,
115 effective_rendering: &citum_schema::template::Rendering,
116 options: &RenderOptions<'_>,
117 fmt: &F,
118) -> Option<String> {
119 if let Some(locator_type) = number_var_to_locator_type(number) {
120 let plural = check_plural(value, &locator_type);
122
123 let term_form = match label_form {
124 citum_schema::template::LabelForm::Long => TermForm::Long,
125 citum_schema::template::LabelForm::Short => TermForm::Short,
126 citum_schema::template::LabelForm::Symbol => TermForm::Symbol,
127 };
128
129 options
130 .locale
131 .resolved_locator_term(&locator_type, plural, &term_form, requested_gender)
132 .map(|t| {
133 let term_str = if crate::values::should_strip_periods(effective_rendering, options)
134 {
135 crate::values::strip_trailing_periods(&t)
136 } else {
137 t
138 };
139 fmt.text(&format!("{term_str} "))
140 })
141 } else {
142 None
143 }
144}
145
146impl ComponentValues for TemplateNumber {
147 fn values<F: crate::render::format::OutputFormat<Output = String>>(
148 &self,
149 reference: &Reference,
150 hints: &ProcHints,
151 options: &RenderOptions<'_>,
152 ) -> Option<ProcValues<F::Output>> {
153 let fmt = F::default();
154
155 let value = resolve_number_value(
156 &self.number,
157 reference,
158 hints,
159 options,
160 self.show_with_locator.unwrap_or(false),
161 );
162
163 value.filter(|s| !s.is_empty()).map(|value| {
164 let effective_rendering = &self.rendering;
166
167 let prefix = if let Some(label_form) = &self.label_form {
169 resolve_number_label(
170 &self.number,
171 label_form,
172 &value,
173 self.gender.clone(),
174 effective_rendering,
175 options,
176 &fmt,
177 )
178 } else {
179 None
180 };
181
182 ProcValues {
183 value,
184 prefix,
185 suffix: None,
186 url: crate::values::resolve_effective_url(
187 self.links.as_ref(),
188 options.config.links.as_ref(),
189 reference,
190 citum_schema::options::LinkAnchor::Component,
191 ),
192 substituted_key: None,
193 pre_formatted: false,
194 }
195 })
196 }
197}
198
199#[must_use]
205pub fn number_var_to_locator_type(
206 var: &NumberVariable,
207) -> Option<citum_schema::citation::LocatorType> {
208 use citum_schema::citation::LocatorType;
209 match var {
210 NumberVariable::Volume => Some(LocatorType::Volume),
211 NumberVariable::Pages => Some(LocatorType::Page),
212 NumberVariable::ChapterNumber => Some(LocatorType::Chapter),
213 NumberVariable::NumberOfPages => Some(LocatorType::Page),
214 NumberVariable::NumberOfVolumes => Some(LocatorType::Volume),
215 NumberVariable::Number
216 | NumberVariable::DocketNumber
217 | NumberVariable::PatentNumber
218 | NumberVariable::StandardNumber
219 | NumberVariable::ReportNumber
220 | NumberVariable::PrintingNumber => Some(LocatorType::Number),
221 NumberVariable::PartNumber => Some(LocatorType::Part),
222 NumberVariable::SupplementNumber => Some(LocatorType::Supplement),
223 NumberVariable::Issue => Some(LocatorType::Issue),
224 NumberVariable::Custom(kind) => Some(LocatorType::Custom(kind.clone())),
225 _ => None,
226 }
227}
228
229#[must_use]
235pub fn check_plural(value: &str, _locator_type: &citum_schema::citation::LocatorType) -> bool {
236 value.contains('–') || value.contains('-') || value.contains(',') || value.contains('&')
239}
240
241#[must_use]
245pub fn format_page_range(
246 pages: &str,
247 format: Option<&citum_schema::options::PageRangeFormat>,
248) -> String {
249 use citum_schema::options::PageRangeFormat;
250
251 let pages = pages.replace('-', "–");
253
254 let Some(format) = format else {
256 return pages; };
258
259 let parts: Vec<&str> = pages.split('–').collect();
261 if parts.len() != 2 {
262 return pages; }
264
265 #[allow(clippy::indexing_slicing, reason = "length checked")]
266 let start = parts[0].trim();
267 #[allow(clippy::indexing_slicing, reason = "length checked")]
268 let end = parts[1].trim();
269
270 let start_num: Option<u32> = start.parse().ok();
272 let end_num: Option<u32> = end.parse().ok();
273
274 match (start_num, end_num) {
275 (Some(s), Some(e)) if e > s => {
276 let formatted_end = match format {
277 PageRangeFormat::Expanded => end.to_string(),
278 PageRangeFormat::Minimal => format_minimal(start, end, 1),
279 PageRangeFormat::MinimalTwo => format_minimal(start, end, 2),
280 PageRangeFormat::Chicago | PageRangeFormat::Chicago16 => format_chicago(s, e),
281 _ => end.to_string(), };
283 format!("{start}–{formatted_end}")
284 }
285 _ => pages, }
287}
288
289#[must_use]
291pub fn format_minimal(start: &str, end: &str, min_digits: usize) -> String {
292 let start_chars: Vec<char> = start.chars().collect();
293 let end_chars: Vec<char> = end.chars().collect();
294
295 if start_chars.len() != end_chars.len() {
296 return end.to_string();
297 }
298
299 let mut first_diff = 0;
301 for (i, (s, e)) in start_chars.iter().zip(end_chars.iter()).enumerate() {
302 if s != e {
303 first_diff = i;
304 break;
305 }
306 }
307
308 let keep_from = first_diff.min(end_chars.len().saturating_sub(min_digits));
310 end_chars
311 .get(keep_from..)
312 .unwrap_or_default()
313 .iter()
314 .collect()
315}
316
317#[must_use]
319pub fn format_chicago(start: u32, end: u32) -> String {
320 if start < 100 || end < 100 {
326 return end.to_string();
327 }
328
329 let start_str = start.to_string();
330 let end_str = end.to_string();
331
332 if start_str.len() != end_str.len() {
333 return end_str;
334 }
335
336 let start_prefix = start / 100;
338 let end_prefix = end / 100;
339
340 if start_prefix != end_prefix {
341 return end_str; }
343
344 format_minimal(&start_str, &end_str, 2)
346}
347
348#[cfg(test)]
349#[allow(
350 clippy::unwrap_used,
351 clippy::expect_used,
352 clippy::panic,
353 clippy::indexing_slicing,
354 clippy::todo,
355 clippy::unimplemented,
356 clippy::unreachable,
357 clippy::get_unwrap,
358 reason = "Panicking is acceptable and often desired in tests."
359)]
360mod tests {
361 use super::*;
362 use citum_schema::options::PageRangeFormat;
363
364 #[test]
365 fn test_format_chicago() {
366 for (start, end, expected) in [
367 (3, 10, "10"),
368 (71, 72, "72"),
369 (96, 117, "117"),
370 (107, 108, "08"),
371 (321, 328, "28"),
372 (1536, 1538, "38"),
373 (107, 208, "208"),
374 (321, 428, "428"),
375 ] {
376 assert_eq!(format_chicago(start, end), expected);
377 }
378 }
379
380 #[test]
381 fn test_format_minimal() {
382 for (start, end, min_digits, expected) in [
383 ("100", "105", 1, "5"),
384 ("100", "105", 2, "05"),
385 ("1536", "1538", 1, "8"),
386 ("1536", "1538", 2, "38"),
387 ("1536", "1538", 4, "1538"),
388 ("12", "15", 1, "5"),
389 ("12", "15", 2, "15"),
390 ("10", "150", 1, "150"),
391 ] {
392 assert_eq!(format_minimal(start, end, min_digits), expected);
393 }
394 }
395
396 #[test]
397 fn test_format_page_range() {
398 for (input, format, expected) in [
399 ("10-15", None, "10–15"),
400 ("10–15", None, "10–15"),
401 ("321-328", None, "321–328"),
402 ("10-15", Some(PageRangeFormat::Expanded), "10–15"),
403 ("42-45", Some(PageRangeFormat::Expanded), "42–45"),
404 ("107-108", Some(PageRangeFormat::Chicago), "107–08"),
405 ("71-72", Some(PageRangeFormat::Chicago), "71–72"),
406 ("321-328", Some(PageRangeFormat::Chicago), "321–28"),
407 ("321-428", Some(PageRangeFormat::Chicago), "321–428"),
408 ("1536-1538", Some(PageRangeFormat::Chicago), "1536–38"),
409 ("100-105", Some(PageRangeFormat::Minimal), "100–5"),
410 ("321-328", Some(PageRangeFormat::Minimal), "321–8"),
411 ("42-45", Some(PageRangeFormat::Minimal), "42–5"),
412 ("12-17", Some(PageRangeFormat::Minimal), "12–7"),
413 ("100-105", Some(PageRangeFormat::MinimalTwo), "100–05"),
414 ("42-45", Some(PageRangeFormat::MinimalTwo), "42–45"),
415 ("10", Some(PageRangeFormat::Chicago), "10"),
416 ("10-5", Some(PageRangeFormat::Chicago), "10–5"),
417 ("X-Y", Some(PageRangeFormat::Chicago), "X–Y"),
418 ("10-15-20", Some(PageRangeFormat::Chicago), "10–15–20"),
419 ] {
420 assert_eq!(format_page_range(input, format.as_ref()), expected);
421 }
422 }
423
424 #[test]
425 fn test_check_plural() {
426 for (value, expected) in [
427 ("1-10", true),
428 ("1–10", true),
429 ("1, 3", true),
430 ("1 & 3", true),
431 ("1", false),
432 ("IV", false),
433 ] {
434 assert_eq!(
435 check_plural(value, &citum_schema::citation::LocatorType::Page),
436 expected
437 );
438 }
439 }
440
441 #[test]
442 fn number_var_to_locator_type_maps_printing_number() {
443 assert_eq!(
444 number_var_to_locator_type(&NumberVariable::PrintingNumber),
445 Some(citum_schema::citation::LocatorType::Number)
446 );
447 }
448
449 #[test]
450 fn number_var_to_locator_type_maps_part_number() {
451 assert_eq!(
452 number_var_to_locator_type(&NumberVariable::PartNumber),
453 Some(citum_schema::citation::LocatorType::Part)
454 );
455 }
456
457 #[test]
458 fn number_var_to_locator_type_maps_supplement_number() {
459 assert_eq!(
460 number_var_to_locator_type(&NumberVariable::SupplementNumber),
461 Some(citum_schema::citation::LocatorType::Supplement)
462 );
463 }
464}