citum_schema_data/reference/
date.rs1use crate::reference::types::RefDate;
7#[cfg(feature = "schema")]
8use schemars::JsonSchema;
9use serde::{Deserialize, Serialize};
10use std::fmt;
11
12#[derive(Debug, Clone, Default, PartialEq)]
27#[cfg_attr(feature = "bindings", derive(specta::Type))]
28pub struct DateValue {
29 pub value: String,
31 #[cfg_attr(feature = "bindings", specta(optional))]
34 pub note: Option<String>,
35}
36
37impl DateValue {
38 pub fn new(value: impl Into<String>) -> Self {
40 Self {
41 value: value.into(),
42 note: None,
43 }
44 }
45
46 pub fn is_empty(&self) -> bool {
48 self.value.is_empty()
49 }
50
51 pub fn parse(&self) -> RefDate {
53 match self.value.parse::<citum_edtf::Edtf>() {
54 Ok(edtf) => RefDate::Edtf(edtf),
55 Err(_) => RefDate::Literal(self.value.clone()),
56 }
57 }
58
59 pub fn year(&self) -> String {
61 match self.parse() {
62 RefDate::Edtf(edtf) => edtf.year().to_string(),
63 RefDate::Literal(_) => String::new(),
64 }
65 }
66
67 pub fn month(&self) -> Option<u32> {
69 match self.parse() {
70 RefDate::Edtf(edtf) => edtf.month(),
71 RefDate::Literal(_) => None,
72 }
73 }
74
75 pub fn day(&self) -> Option<u32> {
77 match self.parse() {
78 RefDate::Edtf(edtf) => edtf.day().filter(|&d| d > 0),
79 RefDate::Literal(_) => None,
80 }
81 }
82
83 pub fn date_parts(&self) -> Option<(i64, Option<u32>, Option<u32>)> {
89 match self.parse() {
90 RefDate::Edtf(edtf) => Some((edtf.year(), edtf.month(), edtf.day().filter(|&d| d > 0))),
91 RefDate::Literal(_) => None,
92 }
93 }
94
95 pub fn is_uncertain(&self) -> bool {
97 self.value.contains('?')
98 }
99
100 pub fn is_approximate(&self) -> bool {
102 self.value.contains('~')
103 }
104
105 pub fn is_range(&self) -> bool {
107 matches!(self.parse(), RefDate::Edtf(edtf) if edtf.is_range())
108 }
109
110 pub fn is_open_range(&self) -> bool {
112 matches!(self.parse(), RefDate::Edtf(edtf) if edtf.is_open_range())
113 }
114
115 pub fn time(&self) -> Option<citum_edtf::Time> {
117 match self.parse() {
118 RefDate::Edtf(edtf) => edtf.time(),
119 _ => None,
120 }
121 }
122
123 pub fn has_time(&self) -> bool {
125 self.time().is_some()
126 }
127}
128
129impl fmt::Display for DateValue {
130 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131 write!(f, "{}", self.value)
132 }
133}
134
135impl From<String> for DateValue {
136 fn from(value: String) -> Self {
137 Self::new(value)
138 }
139}
140
141impl From<&str> for DateValue {
142 fn from(value: &str) -> Self {
143 Self::new(value)
144 }
145}
146
147#[derive(Deserialize)]
151#[serde(deny_unknown_fields)]
152struct DateValueStructured {
153 value: String,
154 #[serde(default)]
155 note: Option<String>,
156}
157
158#[derive(Deserialize)]
159#[serde(untagged)]
160enum DateValueRepr {
161 Scalar(String),
162 Structured(DateValueStructured),
163}
164
165impl<'de> Deserialize<'de> for DateValue {
166 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
167 where
168 D: serde::Deserializer<'de>,
169 {
170 Ok(match DateValueRepr::deserialize(deserializer)? {
171 DateValueRepr::Scalar(value) => DateValue { value, note: None },
172 DateValueRepr::Structured(DateValueStructured { value, note }) => {
173 DateValue { value, note }
174 }
175 })
176 }
177}
178
179impl Serialize for DateValue {
180 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
181 where
182 S: serde::Serializer,
183 {
184 match &self.note {
185 None => serializer.serialize_str(&self.value),
186 Some(note) => {
187 use serde::ser::SerializeMap;
188 let mut map = serializer.serialize_map(Some(2))?;
189 map.serialize_entry("value", &self.value)?;
190 map.serialize_entry("note", note)?;
191 map.end()
192 }
193 }
194 }
195}
196
197#[cfg(feature = "schema")]
198impl JsonSchema for DateValue {
199 fn schema_name() -> std::borrow::Cow<'static, str> {
200 "DateValue".into()
201 }
202
203 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
204 let scalar_schema = generator.subschema_for::<String>();
205 let structured_schema = schemars::json_schema!({
206 "type": "object",
207 "properties": {
208 "value": generator.subschema_for::<String>(),
209 "note": generator.subschema_for::<Option<String>>()
210 },
211 "required": ["value"],
212 "additionalProperties": false
213 });
214 schemars::json_schema!({
215 "oneOf": [scalar_schema, structured_schema]
216 })
217 }
218}
219
220#[cfg(test)]
221#[allow(clippy::unwrap_used, reason = "Panicking is acceptable in tests.")]
222mod tests {
223 use super::*;
224
225 #[test]
226 fn scalar_input_round_trips_byte_identically() {
227 let json = r#""1947""#;
228 let date: DateValue = serde_json::from_str(json).unwrap();
229 assert_eq!(date, DateValue::new("1947"));
230 assert_eq!(serde_json::to_string(&date).unwrap(), json);
231 }
232
233 #[test]
234 fn mapping_form_parses_value_and_note() {
235 let json = r#"{"value":"1947","note":"民国三十六年"}"#;
236 let date: DateValue = serde_json::from_str(json).unwrap();
237 assert_eq!(date.value, "1947");
238 assert_eq!(date.note.as_deref(), Some("民国三十六年"));
239 }
240
241 #[test]
242 fn mapping_form_without_note_defaults_to_none() {
243 let json = r#"{"value":"1947"}"#;
244 let date: DateValue = serde_json::from_str(json).unwrap();
245 assert_eq!(date, DateValue::new("1947"));
246 }
247
248 #[test]
249 fn mapping_form_rejects_unknown_fields() {
250 let json = r#"{"value":"1947","note":"民国三十六年","calendar":"minguo"}"#;
251 let err = serde_json::from_str::<DateValue>(json).unwrap_err();
252 assert!(
253 err.to_string()
254 .contains("did not match any variant of untagged enum")
255 );
256 }
257
258 #[test]
259 fn mapping_form_requires_value() {
260 let json = r#"{"note":"民国三十六年"}"#;
261 assert!(serde_json::from_str::<DateValue>(json).is_err());
262 }
263
264 #[test]
265 fn note_present_serializes_as_mapping_not_scalar() {
266 let date = DateValue {
267 value: "1947".to_string(),
268 note: Some("民国三十六年".to_string()),
269 };
270 let json = serde_json::to_string(&date).unwrap();
271 assert_eq!(json, r#"{"value":"1947","note":"民国三十六年"}"#);
272
273 let round_tripped: DateValue = serde_json::from_str(&json).unwrap();
274 assert_eq!(round_tripped, date);
275 }
276
277 #[test]
278 fn note_is_ignored_by_value_oriented_accessors() {
279 let annotated = DateValue {
283 value: "1947".to_string(),
284 note: Some("民国三十六年".to_string()),
285 };
286 let plain = DateValue::new("1947");
287 assert_eq!(annotated.value, plain.value);
288 assert_eq!(annotated.year(), plain.year());
289 assert_eq!(annotated.is_empty(), plain.is_empty());
290 }
291}