1use serde::Serialize;
6use serde_json::{json, Value};
7
8#[derive(Serialize, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
11#[serde(rename_all = "UPPERCASE")]
12pub enum Status {
13 Ok,
14 Warn,
15 Fail,
16}
17
18#[derive(Serialize)]
20pub struct Check {
21 pub name: &'static str,
22 pub status: Status,
23 pub summary: String,
24 #[serde(skip_serializing_if = "Value::is_null")]
25 pub details: Value,
26}
27
28impl Check {
29 pub(crate) fn new(
30 name: &'static str,
31 status: Status,
32 summary: impl Into<String>,
33 details: Value,
34 ) -> Self {
35 Check {
36 name,
37 status,
38 summary: summary.into(),
39 details,
40 }
41 }
42}
43
44#[derive(Serialize)]
46pub struct DataAuditReport {
47 pub data_dir: String,
48 pub from: i32,
49 pub to: i32,
50 pub symbol_count: usize,
51 pub overall: Status,
52 pub checks: Vec<Check>,
53}
54
55pub fn render_table(report: &DataAuditReport) -> String {
57 let mut out = String::new();
58 out.push_str(&format!(
59 "data-audit: {} [{}..{}] {} symbols\n",
60 report.data_dir, report.from, report.to, report.symbol_count
61 ));
62 out.push_str(&format!("overall: {}\n\n", status_str(report.overall)));
63 for c in &report.checks {
64 out.push_str(&format!(
65 "[{:>4}] {:<16} {}\n",
66 status_str(c.status),
67 c.name,
68 c.summary
69 ));
70 }
71 out
72}
73
74pub(crate) fn status_str(s: Status) -> &'static str {
75 match s {
76 Status::Ok => "OK",
77 Status::Warn => "WARN",
78 Status::Fail => "FAIL",
79 }
80}
81
82pub(crate) fn sample(names: &[&str]) -> Vec<String> {
84 names.iter().take(20).map(|s| s.to_string()).collect()
85}
86
87pub(crate) fn range_or_null(first: i32, last: i32) -> Value {
88 if first == i32::MAX {
89 Value::Null
90 } else {
91 json!({ "first_day": first, "last_day": last })
92 }
93}
94
95pub(crate) fn is_month_end(day: i32) -> bool {
98 let (y, m, d) = (day / 10000, (day / 100) % 100, day % 100);
99 d == days_in_month(y, m)
100}
101
102fn days_in_month(y: i32, m: i32) -> i32 {
103 match m {
104 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
105 4 | 6 | 9 | 11 => 30,
106 2 if (y % 4 == 0 && y % 100 != 0) || y % 400 == 0 => 29,
107 2 => 28,
108 _ => 0,
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 #[test]
117 fn month_end_and_days_in_month() {
118 assert!(is_month_end(20240229)); assert!(is_month_end(20230228)); assert!(!is_month_end(20240228)); assert!(is_month_end(20240131));
122 assert!(is_month_end(20240430));
123 assert!(!is_month_end(20240415));
124 assert_eq!(days_in_month(2024, 2), 29);
125 assert_eq!(days_in_month(2023, 2), 28);
126 assert_eq!(days_in_month(2000, 2), 29); assert_eq!(days_in_month(1900, 2), 28); assert_eq!(days_in_month(2024, 4), 30);
129 assert_eq!(days_in_month(2024, 7), 31);
130 assert_eq!(days_in_month(2024, 13), 0);
131 }
132
133 #[test]
134 fn sample_and_range_helpers() {
135 assert_eq!(sample(&["a", "b"]), vec!["a".to_string(), "b".to_string()]);
136 assert!(range_or_null(i32::MAX, i32::MIN).is_null());
137 assert!(!range_or_null(20240101, 20240102).is_null());
138 assert_eq!(status_str(Status::Ok), "OK");
139 assert_eq!(status_str(Status::Warn), "WARN");
140 assert_eq!(status_str(Status::Fail), "FAIL");
141 }
142}