citeworks_csl/
dates.rs

1//! Types and utilities for dates complex values.
2
3use std::{
4	collections::BTreeMap,
5	fmt::{Debug, Display},
6	num::ParseIntError,
7	str::FromStr,
8};
9
10use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer};
11
12use crate::ordinaries::OrdinaryValue;
13
14/// Date formats.
15///
16/// Date fields can be expressed in different forms.
17///
18/// The first serialises as an array format, containing either a single date in
19/// a double-nested array keyed under the `date-parts` field, or a date range as
20/// two inner arrays in the `date-parts` outer array. In this library, array
21/// singles and array ranges are represented separately as `Single` and `Range`.
22///
23/// The second form is a field named `raw` with a string representation of the
24/// date in arbitrary or human formats, which citation software may attempt to
25/// recognise. This library doesn't attempt to parse raw dates.
26///
27/// [EDTF] (Extended Date/Time Format) is a structured string format for dates,
28/// datetimes, and ranges established by the United States of America's Library
29/// of Congress.
30///
31/// All forms may also have any of the [metadata or less-precise fields][meta].
32///
33/// [EDTF]: https://www.librarianshipstudies.com/2016/05/extended-date-time-format-edtf.html
34/// [meta]: DateMeta
35#[derive(Debug, Clone, Hash, Eq, PartialEq)]
36pub enum Date {
37	/// Structured single date
38	Single {
39		/// Date as a [year, month, day] array
40		date: DateParts,
41
42		/// Additional date (meta)data
43		meta: DateMeta,
44	},
45
46	/// Structured date range
47	Range {
48		/// Start date as a [year, month, day] array
49		start: DateParts,
50
51		/// End date as a [year, month, day] array
52		end: DateParts,
53
54		/// Additional date (meta)data
55		meta: DateMeta,
56	},
57
58	/// Raw
59	Raw {
60		/// Date as a string in arbitrary format
61		date: String,
62
63		/// Additional date (meta)data
64		meta: DateMeta,
65	},
66
67	/// EDTF
68	Edtf {
69		/// Date in EDTF string format
70		date: String,
71
72		/// Additional date (meta)data
73		meta: DateMeta,
74	},
75}
76
77#[derive(Debug, Default, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
78#[serde(rename_all = "kebab-case")]
79struct DateInternal {
80	#[serde(default, skip_serializing_if = "Vec::is_empty")]
81	date_parts: Vec<DateParts>,
82
83	#[serde(default, skip_serializing_if = "Option::is_none")]
84	season: Option<Season>,
85
86	#[serde(default, skip_serializing_if = "Option::is_none")]
87	circa: Option<Circa>,
88
89	#[serde(default, skip_serializing_if = "Option::is_none")]
90	literal: Option<String>,
91
92	#[serde(default, skip_serializing_if = "Option::is_none")]
93	raw: Option<String>,
94
95	#[serde(default, skip_serializing_if = "Option::is_none")]
96	edtf: Option<String>,
97
98	#[serde(flatten)]
99	extra: BTreeMap<String, OrdinaryValue>,
100}
101
102impl Date {
103	/// Get the [DateMeta] of any variant.
104	pub fn meta(&self) -> &DateMeta {
105		match self {
106			Self::Single { meta, .. }
107			| Self::Range { meta, .. }
108			| Self::Raw { meta, .. }
109			| Self::Edtf { meta, .. } => meta,
110		}
111	}
112}
113
114impl Serialize for Date {
115	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
116	where
117		S: Serializer,
118	{
119		let meta = self.meta().clone();
120		let mut internal = DateInternal {
121			season: meta.season,
122			circa: meta.circa,
123			literal: meta.literal,
124			extra: meta.extra,
125			..Default::default()
126		};
127
128		match self {
129			Self::Single { date, .. } => {
130				internal.date_parts = vec![*date];
131			}
132			Self::Range { start, end, .. } => {
133				internal.date_parts = vec![*start, *end];
134			}
135			Self::Raw { date, .. } => {
136				internal.raw = Some(date.clone());
137			}
138			Self::Edtf { date, .. } => {
139				internal.edtf = Some(date.clone());
140			}
141		}
142
143		internal.serialize(serializer)
144	}
145}
146
147impl<'de> Deserialize<'de> for Date {
148	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
149	where
150		D: Deserializer<'de>,
151		D::Error: serde::de::Error,
152	{
153		let internal = DateInternal::deserialize(deserializer)?;
154
155		if internal.date_parts.len() == 1 {
156			Ok(Self::Single {
157				date: internal.date_parts[0],
158				meta: DateMeta::from_internal(internal),
159			})
160		} else if internal.date_parts.len() == 2 {
161			Ok(Self::Range {
162				start: internal.date_parts[0],
163				end: internal.date_parts[1],
164				meta: DateMeta::from_internal(internal),
165			})
166		} else if let Some(date) = &internal.edtf {
167			Ok(Self::Edtf {
168				date: date.clone(),
169				meta: DateMeta::from_internal(internal),
170			})
171		} else if let Some(date) = &internal.raw {
172			Ok(Self::Raw {
173				date: date.clone(),
174				meta: DateMeta::from_internal(internal),
175			})
176		} else {
177			Err(D::Error::custom("unknown date format".to_string()))
178		}
179	}
180}
181
182/// The core "date-parts" of a date complex type.
183///
184/// In CSL-JSON this is an array `[year, month, day]`.
185#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Serialize, Deserialize)]
186#[serde(try_from = "DatePartsInternal", into = "DatePartsInternal")]
187pub struct DateParts {
188	/// Year, in the Gregorian calendar
189	pub year: i64,
190
191	/// Month, starting from 1
192	pub month: Option<u8>,
193
194	/// Day of the month, starting from 1
195	pub day: Option<u8>,
196}
197
198#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
199struct DatePartsInternal(
200	StrumI64,
201	#[serde(default, skip_serializing_if = "Option::is_none")] Option<StrumU8>,
202	#[serde(default, skip_serializing_if = "Option::is_none")] Option<StrumU8>,
203);
204
205#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
206#[serde(untagged)]
207enum StrumI64 {
208	String(String),
209	Num(i64),
210}
211
212#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
213#[serde(untagged)]
214enum StrumU8 {
215	String(String),
216	Num(u8),
217}
218
219impl TryFrom<StrumI64> for i64 {
220	type Error = ParseIntError;
221
222	fn try_from(value: StrumI64) -> Result<Self, Self::Error> {
223		match value {
224			StrumI64::String(s) => s.parse(),
225			StrumI64::Num(t) => Ok(t),
226		}
227	}
228}
229
230impl TryFrom<StrumU8> for u8 {
231	type Error = ParseIntError;
232
233	fn try_from(value: StrumU8) -> Result<Self, Self::Error> {
234		match value {
235			StrumU8::String(s) => s.parse(),
236			StrumU8::Num(t) => Ok(t),
237		}
238	}
239}
240
241impl TryFrom<DatePartsInternal> for DateParts {
242	type Error = ParseIntError;
243
244	fn try_from(
245		DatePartsInternal(year, month, day): DatePartsInternal,
246	) -> Result<Self, Self::Error> {
247		Ok(Self {
248			year: year.try_into()?,
249			month: month.map(|m| m.try_into()).transpose()?,
250			day: day.map(|d| d.try_into()).transpose()?,
251		})
252	}
253}
254
255impl From<DateParts> for DatePartsInternal {
256	fn from(parts: DateParts) -> Self {
257		Self(
258			StrumI64::Num(parts.year),
259			parts.month.map(StrumU8::Num),
260			parts.day.map(StrumU8::Num),
261		)
262	}
263}
264
265#[derive(Debug, Default, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
266/// Date metadata or less-precise fields.
267pub struct DateMeta {
268	/// A season.
269	#[serde(default, skip_serializing_if = "Option::is_none")]
270	pub season: Option<Season>,
271
272	/// Unprecise date.
273	///
274	/// A number will be considered a year.
275	///
276	/// Can also take a boolean to mark the enclosing date as approximate.
277	#[serde(default, skip_serializing_if = "Option::is_none")]
278	pub circa: Option<Circa>, // String, number, bool
279
280	/// Full date in whatever format.
281	#[serde(default, skip_serializing_if = "Option::is_none")]
282	pub literal: Option<String>,
283
284	/// Date fields not defined above.
285	#[serde(flatten)]
286	pub extra: BTreeMap<String, OrdinaryValue>,
287}
288
289impl DateMeta {
290	fn from_internal(internal: DateInternal) -> Self {
291		Self {
292			season: internal.season,
293			circa: internal.circa,
294			literal: internal.literal,
295			extra: internal.extra,
296		}
297	}
298}
299
300/// Circa field of date metadata
301///
302/// This has multiple uses:
303/// - it can be a year or arbitrary string, interpreted like `ca. 2008`; or
304/// - it can be a boolean (generally only `true`) to indicate that the
305///   containing date is itself approximate.
306#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
307#[serde(untagged)]
308pub enum Circa {
309	/// Arbitrary string for the circa value.
310	Arbitrary(String),
311
312	/// Approximate year.
313	Year(i64),
314
315	/// Whether the date itself is approximate.
316	Bool(bool),
317}
318
319impl Circa {
320	/// If the [Circa] is an arbitrary string, return it.
321	pub fn as_arbitrary(&self) -> Option<&str> {
322		if let Self::Arbitrary(str) = self {
323			Some(str.as_ref())
324		} else {
325			None
326		}
327	}
328
329	/// If the [Circa] is a numerical year, return it.
330	pub fn as_year(&self) -> Option<i64> {
331		if let Self::Year(num) = self {
332			Some(*num)
333		} else {
334			None
335		}
336	}
337
338	/// If the [Circa] is a boolean, return it.
339	pub fn as_bool(&self) -> Option<bool> {
340		if let Self::Bool(b) = self {
341			Some(*b)
342		} else {
343			None
344		}
345	}
346}
347
348/// Season value for approximate dates.
349///
350/// This does not contain information as to where the season is, e.g.
351/// Winter in the north hemisphere could be Summer in the south.
352#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)]
353pub enum Season {
354	/// Spring season, or `season-01` in CSL.
355	Spring,
356
357	/// Summer season, or `season-02` in CSL.
358	Summer,
359
360	/// Autumn season, or `season-03` in CSL.
361	Autumn,
362
363	/// Winter season, or `season-04` in CSL.
364	Winter,
365}
366
367impl Display for Season {
368	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
369		write!(
370			f,
371			"{}",
372			match self {
373				Self::Spring => "spring",
374				Self::Summer => "summer",
375				Self::Autumn => "autumn",
376				Self::Winter => "winter",
377			}
378		)
379	}
380}
381
382impl FromStr for Season {
383	type Err = String;
384
385	fn from_str(s: &str) -> Result<Self, Self::Err> {
386		match s.to_lowercase().as_str() {
387			"spring" | "season-01" => Ok(Self::Spring),
388			"summer" | "season-02" => Ok(Self::Summer),
389			"autumn" | "season-03" => Ok(Self::Autumn),
390			"winter" | "season-04" => Ok(Self::Winter),
391			other => Err(format!("unknown season: {other:?}")),
392		}
393	}
394}
395
396impl Serialize for Season {
397	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
398	where
399		S: Serializer,
400	{
401		let s = self.to_string();
402		s.serialize(serializer)
403	}
404}
405
406impl<'de> Deserialize<'de> for Season {
407	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
408	where
409		D: Deserializer<'de>,
410	{
411		let s = String::deserialize(deserializer)?;
412		Season::from_str(&s).map_err(D::Error::custom)
413	}
414}