1use std::{marker::PhantomData, num::NonZeroI32};
2
3#[cfg(feature = "chrono")]
4use chrono::{Datelike, NaiveDate, NaiveDateTime, Timelike};
5use serde::{Deserialize, Serialize};
6use thiserror::Error;
7use time::{Date, PrimitiveDateTime};
8
9#[derive(Debug, Error, Clone, PartialEq, Eq)]
11pub enum InputError {
12 #[error("{kind} must be strictly positive, got {value}")]
14 NonPositiveId { kind: &'static str, value: i32 },
15 #[error("year span start {start} must be less than or equal to end {end}")]
17 InvalidYearSpan { start: i32, end: i32 },
18 #[error("parent reference must be -1 (root) or positive id, got {value}")]
20 InvalidParentRef { value: i32 },
21 #[cfg(feature = "chrono")]
23 #[error("chrono value is out of range for {kind}")]
24 ChronoOutOfRange { kind: &'static str },
25 #[cfg(feature = "chrono")]
27 #[error("sub-second precision is not supported for chrono conversion")]
28 ChronoSubsecondPrecision,
29}
30
31pub trait IdKind {
33 const NAME: &'static str;
35}
36
37pub struct Id<K>(NonZeroI32, PhantomData<K>);
39
40impl<K> Copy for Id<K> {}
41
42impl<K> Clone for Id<K> {
43 fn clone(&self) -> Self {
44 *self
45 }
46}
47
48impl<K> std::fmt::Debug for Id<K> {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 f.debug_tuple("Id").field(&self.0.get()).finish()
51 }
52}
53
54impl<K> PartialEq for Id<K> {
55 fn eq(&self, other: &Self) -> bool {
56 self.0 == other.0
57 }
58}
59
60impl<K> Eq for Id<K> {}
61
62impl<K> std::hash::Hash for Id<K> {
63 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
64 self.0.hash(state);
65 }
66}
67
68impl<K> PartialOrd for Id<K> {
69 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
70 Some(self.cmp(other))
71 }
72}
73
74impl<K> Ord for Id<K> {
75 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
76 self.0.cmp(&other.0)
77 }
78}
79
80impl<K: IdKind> Id<K> {
81 pub fn new(value: i32) -> Result<Self, InputError> {
83 if value <= 0 {
84 return Err(InputError::NonPositiveId {
85 kind: K::NAME,
86 value,
87 });
88 }
89
90 let raw =
91 NonZeroI32::new(value).expect("strictly positive value always produces NonZeroI32");
92 Ok(Self(raw, PhantomData))
93 }
94
95 #[must_use]
99 #[inline]
100 pub const fn new_const(value: i32) -> Self {
101 if value <= 0 {
102 panic!("identifier must be strictly positive");
103 }
104
105 match NonZeroI32::new(value) {
106 Some(raw) => Self(raw, PhantomData),
107 None => panic!("identifier must be strictly positive"),
108 }
109 }
110
111 #[must_use]
113 #[inline]
114 pub fn get(self) -> i32 {
115 self.0.get()
116 }
117}
118
119impl<K: IdKind> TryFrom<i32> for Id<K> {
120 type Error = InputError;
121
122 fn try_from(value: i32) -> Result<Self, Self::Error> {
123 Self::new(value)
124 }
125}
126
127impl<K: IdKind> From<Id<K>> for i32 {
128 fn from(value: Id<K>) -> Self {
129 value.get()
130 }
131}
132
133impl<K: IdKind> Serialize for Id<K> {
134 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
135 where
136 S: serde::Serializer,
137 {
138 serializer.serialize_i32(self.get())
139 }
140}
141
142impl<'de, K: IdKind> Deserialize<'de> for Id<K> {
143 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
144 where
145 D: serde::Deserializer<'de>,
146 {
147 let raw = i32::deserialize(deserializer)?;
148 Self::new(raw).map_err(serde::de::Error::custom)
149 }
150}
151
152#[doc(hidden)]
153pub enum PublicationIdKind {}
154impl IdKind for PublicationIdKind {
155 const NAME: &'static str = "publication_id";
156}
157pub type PublicationId = Id<PublicationIdKind>;
159
160#[doc(hidden)]
161pub enum DatasetIdKind {}
162impl IdKind for DatasetIdKind {
163 const NAME: &'static str = "dataset_id";
164}
165pub type DatasetId = Id<DatasetIdKind>;
167
168#[doc(hidden)]
169pub enum CategoryIdKind {}
170impl IdKind for CategoryIdKind {
171 const NAME: &'static str = "category_id";
172}
173pub type CategoryId = Id<CategoryIdKind>;
175
176#[doc(hidden)]
177pub enum IndicatorIdKind {}
178impl IdKind for IndicatorIdKind {
179 const NAME: &'static str = "indicator_id";
180}
181pub type IndicatorId = Id<IndicatorIdKind>;
183
184#[doc(hidden)]
185pub enum MeasureIdKind {}
186impl IdKind for MeasureIdKind {
187 const NAME: &'static str = "measure_id";
188}
189pub type MeasureId = Id<MeasureIdKind>;
191
192#[doc(hidden)]
193pub enum UnitIdKind {}
194impl IdKind for UnitIdKind {
195 const NAME: &'static str = "unit_id";
196}
197pub type UnitId = Id<UnitIdKind>;
199
200#[doc(hidden)]
201pub enum RowIdKind {}
202impl IdKind for RowIdKind {
203 const NAME: &'static str = "row_id";
204}
205pub type RowId = Id<RowIdKind>;
207
208#[doc(hidden)]
209pub enum PeriodIdKind {}
210impl IdKind for PeriodIdKind {
211 const NAME: &'static str = "period_id";
212}
213pub type PeriodId = Id<PeriodIdKind>;
215
216#[doc(hidden)]
217pub enum ColumnIdKind {}
218impl IdKind for ColumnIdKind {
219 const NAME: &'static str = "column_id";
220}
221pub type ColumnId = Id<ColumnIdKind>;
223
224#[doc(hidden)]
225pub enum ElementIdKind {}
226impl IdKind for ElementIdKind {
227 const NAME: &'static str = "element_id";
228}
229pub type ElementId = Id<ElementIdKind>;
231
232#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
234pub enum ParentRef<T> {
235 Root,
237 Id(T),
239}
240
241impl<T> ParentRef<T>
242where
243 T: TryFrom<i32, Error = InputError> + Copy,
244{
245 pub fn new(value: i32) -> Result<Self, InputError> {
247 match value {
248 -1 => Ok(Self::Root),
249 value if value <= 0 => Err(InputError::InvalidParentRef { value }),
250 _ => T::try_from(value).map(Self::Id),
251 }
252 }
253
254 #[must_use]
256 #[inline]
257 pub fn is_root(self) -> bool {
258 matches!(self, Self::Root)
259 }
260}
261
262impl<T: Copy> ParentRef<T> {
263 #[must_use]
265 #[inline]
266 pub fn id(self) -> Option<T> {
267 match self {
268 Self::Root => None,
269 Self::Id(value) => Some(value),
270 }
271 }
272}
273
274impl<T> Serialize for ParentRef<T>
275where
276 T: Copy + Into<i32>,
277{
278 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
279 where
280 S: serde::Serializer,
281 {
282 let raw = match self {
283 Self::Root => -1,
284 Self::Id(value) => (*value).into(),
285 };
286 serializer.serialize_i32(raw)
287 }
288}
289
290impl<'de, T> Deserialize<'de> for ParentRef<T>
291where
292 T: TryFrom<i32, Error = InputError> + Copy,
293{
294 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
295 where
296 D: serde::Deserializer<'de>,
297 {
298 let raw = i32::deserialize(deserializer)?;
299 Self::new(raw).map_err(serde::de::Error::custom)
300 }
301}
302
303#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
305#[serde(transparent)]
306pub struct Year(i32);
307
308impl Year {
309 #[must_use]
311 #[inline]
312 pub const fn new(value: i32) -> Self {
313 Self(value)
314 }
315
316 #[must_use]
318 #[inline]
319 pub fn get(self) -> i32 {
320 self.0
321 }
322}
323
324impl From<Year> for i32 {
325 fn from(value: Year) -> Self {
326 value.get()
327 }
328}
329
330const ISO_DATETIME_FORMAT: &[time::format_description::FormatItem<'static>] =
331 time::macros::format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]");
332const DMY_DATE_FORMAT: &[time::format_description::FormatItem<'static>] =
333 time::macros::format_description!("[day].[month].[year]");
334
335#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
337pub struct IsoDateTime(PrimitiveDateTime);
338
339impl IsoDateTime {
340 #[must_use]
342 #[inline]
343 pub const fn new(value: PrimitiveDateTime) -> Self {
344 Self(value)
345 }
346
347 pub fn parse(value: &str) -> Result<Self, time::error::Parse> {
349 PrimitiveDateTime::parse(value, ISO_DATETIME_FORMAT).map(Self)
350 }
351
352 #[must_use]
354 #[inline]
355 pub const fn get(self) -> PrimitiveDateTime {
356 self.0
357 }
358
359 #[cfg(feature = "chrono")]
361 pub fn try_from_chrono(value: NaiveDateTime) -> Result<Self, InputError> {
362 Self::try_from(value)
363 }
364
365 #[cfg(feature = "chrono")]
367 pub fn try_to_chrono(self) -> Result<NaiveDateTime, InputError> {
368 let date = self.0.date();
369 let chrono_date = NaiveDate::from_ymd_opt(
370 date.year(),
371 u32::from(u8::from(date.month())),
372 u32::from(date.day()),
373 )
374 .ok_or(InputError::ChronoOutOfRange { kind: "date" })?;
375
376 let time = self.0.time();
377 chrono_date
378 .and_hms_nano_opt(
379 u32::from(time.hour()),
380 u32::from(time.minute()),
381 u32::from(time.second()),
382 time.nanosecond(),
383 )
384 .ok_or(InputError::ChronoOutOfRange { kind: "datetime" })
385 }
386}
387
388impl Serialize for IsoDateTime {
389 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
390 where
391 S: serde::Serializer,
392 {
393 let value = self
394 .0
395 .format(ISO_DATETIME_FORMAT)
396 .map_err(serde::ser::Error::custom)?;
397 serializer.serialize_str(&value)
398 }
399}
400
401impl<'de> Deserialize<'de> for IsoDateTime {
402 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
403 where
404 D: serde::Deserializer<'de>,
405 {
406 let value = <&str>::deserialize(deserializer)?;
407 Self::parse(value).map_err(serde::de::Error::custom)
408 }
409}
410
411impl std::fmt::Display for IsoDateTime {
412 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
413 let value = self
414 .0
415 .format(ISO_DATETIME_FORMAT)
416 .map_err(|_| std::fmt::Error)?;
417 f.write_str(&value)
418 }
419}
420
421#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
423pub struct DmyDate(Date);
424
425impl DmyDate {
426 #[must_use]
428 #[inline]
429 pub const fn new(value: Date) -> Self {
430 Self(value)
431 }
432
433 pub fn parse(value: &str) -> Result<Self, time::error::Parse> {
435 Date::parse(value, DMY_DATE_FORMAT).map(Self)
436 }
437
438 #[must_use]
440 #[inline]
441 pub const fn get(self) -> Date {
442 self.0
443 }
444
445 #[cfg(feature = "chrono")]
447 pub fn try_from_chrono(value: NaiveDate) -> Result<Self, InputError> {
448 Self::try_from(value)
449 }
450
451 #[cfg(feature = "chrono")]
453 pub fn try_to_chrono(self) -> Result<NaiveDate, InputError> {
454 let date = self.0;
455 NaiveDate::from_ymd_opt(
456 date.year(),
457 u32::from(u8::from(date.month())),
458 u32::from(date.day()),
459 )
460 .ok_or(InputError::ChronoOutOfRange { kind: "date" })
461 }
462}
463
464impl Serialize for DmyDate {
465 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
466 where
467 S: serde::Serializer,
468 {
469 let value = self
470 .0
471 .format(DMY_DATE_FORMAT)
472 .map_err(serde::ser::Error::custom)?;
473 serializer.serialize_str(&value)
474 }
475}
476
477impl<'de> Deserialize<'de> for DmyDate {
478 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
479 where
480 D: serde::Deserializer<'de>,
481 {
482 let value = <&str>::deserialize(deserializer)?;
483 Self::parse(value).map_err(serde::de::Error::custom)
484 }
485}
486
487impl std::fmt::Display for DmyDate {
488 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
489 let value = self
490 .0
491 .format(DMY_DATE_FORMAT)
492 .map_err(|_| std::fmt::Error)?;
493 f.write_str(&value)
494 }
495}
496
497#[cfg(feature = "chrono")]
498impl TryFrom<NaiveDateTime> for IsoDateTime {
499 type Error = InputError;
500
501 fn try_from(value: NaiveDateTime) -> Result<Self, Self::Error> {
502 if value.nanosecond() != 0 {
503 return Err(InputError::ChronoSubsecondPrecision);
504 }
505
506 let month = chrono_month_to_time(value.month())?;
507 let day =
508 u8::try_from(value.day()).map_err(|_| InputError::ChronoOutOfRange { kind: "day" })?;
509 let date = Date::from_calendar_date(value.year(), month, day)
510 .map_err(|_| InputError::ChronoOutOfRange { kind: "date" })?;
511 let hour = u8::try_from(value.hour())
512 .map_err(|_| InputError::ChronoOutOfRange { kind: "hour" })?;
513 let minute = u8::try_from(value.minute())
514 .map_err(|_| InputError::ChronoOutOfRange { kind: "minute" })?;
515 let second = u8::try_from(value.second())
516 .map_err(|_| InputError::ChronoOutOfRange { kind: "second" })?;
517 let time = time::Time::from_hms(hour, minute, second)
518 .map_err(|_| InputError::ChronoOutOfRange { kind: "time" })?;
519
520 Ok(Self::new(PrimitiveDateTime::new(date, time)))
521 }
522}
523
524#[cfg(feature = "chrono")]
525impl TryFrom<NaiveDate> for DmyDate {
526 type Error = InputError;
527
528 fn try_from(value: NaiveDate) -> Result<Self, Self::Error> {
529 let month = chrono_month_to_time(value.month())?;
530 let day =
531 u8::try_from(value.day()).map_err(|_| InputError::ChronoOutOfRange { kind: "day" })?;
532 Date::from_calendar_date(value.year(), month, day)
533 .map(Self::new)
534 .map_err(|_| InputError::ChronoOutOfRange { kind: "date" })
535 }
536}
537
538#[cfg(feature = "chrono")]
539fn chrono_month_to_time(value: u32) -> Result<time::Month, InputError> {
540 let month = u8::try_from(value).map_err(|_| InputError::ChronoOutOfRange { kind: "month" })?;
541 time::Month::try_from(month).map_err(|_| InputError::ChronoOutOfRange { kind: "month" })
542}
543
544#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
546#[serde(rename_all = "lowercase")]
547pub enum Periodicity {
548 Month,
550 Quarter,
552 Year,
554}
555
556#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
558pub struct YearSpan {
559 start: Year,
560 end: Year,
561}
562
563impl YearSpan {
564 pub fn new(start: Year, end: Year) -> Result<Self, InputError> {
566 if start > end {
567 return Err(InputError::InvalidYearSpan {
568 start: start.get(),
569 end: end.get(),
570 });
571 }
572
573 Ok(Self { start, end })
574 }
575
576 #[must_use]
578 #[inline]
579 pub fn start(self) -> Year {
580 self.start
581 }
582
583 #[must_use]
585 #[inline]
586 pub fn end(self) -> Year {
587 self.end
588 }
589}