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