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