1use serde::{Deserialize, Serialize};
7use std::time::{SystemTime, UNIX_EPOCH};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct RunManifest {
12 pub version: String,
14 pub git_hash: Option<String>,
16 pub timestamp: String,
18 pub platform: PlatformInfo,
20 pub config: SearchConfigInfo,
22 pub results: Vec<MatchInfo>,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct PlatformInfo {
29 pub os: String,
31 pub arch: String,
33 pub rust_version: String,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct SearchConfigInfo {
40 pub target: f64,
42 pub level: f32,
44 pub max_lhs_complexity: u32,
46 pub max_rhs_complexity: u32,
48 pub deterministic: bool,
50 pub parallel: bool,
52 pub max_error: f64,
54 pub max_matches: usize,
56 pub ranking_mode: String,
58 pub user_constants: Vec<UserConstantInfo>,
60 pub excluded_symbols: Vec<String>,
62 pub allowed_symbols: Option<Vec<String>>,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct UserConstantInfo {
69 pub name: String,
71 pub value: f64,
73 pub description: String,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct MatchInfo {
80 pub lhs_postfix: String,
82 pub rhs_postfix: String,
84 pub lhs_infix: String,
86 pub rhs_infix: String,
88 pub error: f64,
90 pub is_exact: bool,
92 pub complexity: u32,
94 pub x_value: f64,
96 pub stability: Option<f64>,
98}
99
100impl RunManifest {
101 pub fn new(config: SearchConfigInfo, results: Vec<MatchInfo>) -> Self {
103 let timestamp = SystemTime::now()
104 .duration_since(UNIX_EPOCH)
105 .map(|d| {
106 let secs = d.as_secs();
107 chrono_like_timestamp(secs)
109 })
110 .unwrap_or_else(|_| "unknown".to_string());
111
112 Self {
113 version: env!("CARGO_PKG_VERSION").to_string(),
114 git_hash: get_git_hash(),
115 timestamp,
116 platform: PlatformInfo::current(),
117 config,
118 results,
119 }
120 }
121
122 pub fn to_json(&self) -> Result<String, serde_json::Error> {
124 serde_json::to_string_pretty(self)
125 }
126
127 pub fn to_json_compact(&self) -> Result<String, serde_json::Error> {
129 serde_json::to_string(self)
130 }
131}
132
133impl PlatformInfo {
134 pub fn current() -> Self {
136 Self {
137 os: std::env::consts::OS.to_string(),
138 arch: std::env::consts::ARCH.to_string(),
139 rust_version: rustc_version().unwrap_or_else(|| "unknown".to_string()),
140 }
141 }
142}
143
144fn get_git_hash() -> Option<String> {
146 option_env!("GIT_HASH").map(|s| s.to_string()).or_else(|| {
148 #[cfg(debug_assertions)]
150 {
151 std::process::Command::new("git")
152 .args(["rev-parse", "--short", "HEAD"])
153 .output()
154 .ok()
155 .and_then(|o| String::from_utf8(o.stdout).ok())
156 .map(|s| s.trim().to_string())
157 }
158 #[cfg(not(debug_assertions))]
159 {
160 None
161 }
162 })
163}
164
165fn rustc_version() -> Option<String> {
167 #[cfg(debug_assertions)]
169 {
170 std::process::Command::new("rustc")
171 .arg("--version")
172 .output()
173 .ok()
174 .and_then(|o| String::from_utf8(o.stdout).ok())
175 .map(|s| s.trim().to_string())
176 }
177 #[cfg(not(debug_assertions))]
178 {
179 None
180 }
181}
182
183fn chrono_like_timestamp(secs: u64) -> String {
185 let days = secs / 86400;
187 let remaining = secs % 86400;
188 let hours = remaining / 3600;
189 let minutes = (remaining % 3600) / 60;
190 let seconds = remaining % 60;
191
192 let (year, month, day) = days_to_ymd(days);
195
196 format!(
197 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
198 year, month, day, hours, minutes, seconds
199 )
200}
201
202fn days_to_ymd(days: u64) -> (i32, u32, u32) {
204 let mut year = 1970_i32;
206 let mut remaining_days = days as i64;
207
208 loop {
209 let days_in_year = if is_leap_year(year) { 366 } else { 365 };
210 if remaining_days < days_in_year {
211 break;
212 }
213 remaining_days -= days_in_year;
214 year += 1;
215 }
216
217 let days_in_months = if is_leap_year(year) {
218 [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
219 } else {
220 [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
221 };
222
223 let mut month = 1_u32;
224 for &days_in_month in &days_in_months {
225 if remaining_days < days_in_month as i64 {
226 break;
227 }
228 remaining_days -= days_in_month as i64;
229 month += 1;
230 }
231
232 let day = (remaining_days + 1) as u32; (year, month, day)
234}
235
236fn is_leap_year(year: i32) -> bool {
237 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 #[test]
245 fn test_timestamp_format() {
246 let ts = chrono_like_timestamp(1705318245);
248 assert!(ts.starts_with("2024-01-"));
249 assert!(ts.ends_with("Z"));
250 }
251
252 #[test]
253 fn test_leap_year() {
254 assert!(is_leap_year(2024));
255 assert!(!is_leap_year(2023));
256 assert!(!is_leap_year(1900));
257 assert!(is_leap_year(2000));
258 }
259
260 #[test]
261 fn test_timestamp_leap_year_feb29() {
262 let ts = chrono_like_timestamp(951782400);
264 assert!(ts.starts_with("2000-02-29"), "got: {}", ts);
265 }
266
267 #[test]
268 fn test_timestamp_year_boundary() {
269 let ts = chrono_like_timestamp(946684799);
271 assert!(ts.starts_with("1999-12-31"), "got: {}", ts);
272 let ts2 = chrono_like_timestamp(946684800);
274 assert!(ts2.starts_with("2000-01-01"), "got: {}", ts2);
275 }
276
277 #[test]
278 fn test_leap_year_century_rules() {
279 assert!(!is_leap_year(1900));
281 assert!(!is_leap_year(2100));
283 assert!(is_leap_year(2000));
285 assert!(is_leap_year(2400));
287 }
288}