doge_runtime/stdlib/
nap.rs1use std::sync::OnceLock;
10use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
11
12use bigdecimal::ToPrimitive;
13
14use crate::error::{DogeError, DogeResult};
15use crate::stdlib::str_arg;
16use crate::value::Value;
17
18const SECS_PER_DAY: i64 = 86_400;
20
21static MONO_ORIGIN: OnceLock<Instant> = OnceLock::new();
25
26fn numeric(fname: &str, v: &Value) -> DogeResult<f64> {
29 match v {
30 Value::Int(n) => n
31 .to_f64()
32 .ok_or_else(|| DogeError::overflow(format!("{n} is too large for nap.{fname}"))),
33 Value::Float(f) => Ok(*f),
34 _ => Err(DogeError::type_error(format!(
35 "nap.{fname} needs a number, got {}",
36 v.describe()
37 ))),
38 }
39}
40
41pub fn nap_now() -> DogeResult {
45 let secs = match SystemTime::now().duration_since(UNIX_EPOCH) {
46 Ok(elapsed) => elapsed.as_secs_f64(),
47 Err(before) => -before.duration().as_secs_f64(),
48 };
49 Ok(Value::Float(secs))
50}
51
52pub fn nap_mono() -> DogeResult {
56 let origin = MONO_ORIGIN.get_or_init(Instant::now);
57 Ok(Value::Float(origin.elapsed().as_secs_f64()))
58}
59
60pub fn nap_rest(seconds: &Value) -> DogeResult {
64 let secs = numeric("rest", seconds)?;
65 if !secs.is_finite() || secs < 0.0 {
66 return Err(DogeError::value_error(format!(
67 "cannot rest for {secs} seconds — the duration must be finite and not negative"
68 )));
69 }
70 if secs > Duration::MAX.as_secs_f64() {
71 return Err(DogeError::value_error(
72 "cannot rest that long — the duration is out of range",
73 ));
74 }
75 std::thread::sleep(Duration::from_secs_f64(secs));
76 Ok(Value::None)
77}
78
79pub fn nap_stamp(secs: &Value) -> DogeResult {
84 let secs = numeric("stamp", secs)?;
85 if !secs.is_finite() || secs < i64::MIN as f64 || secs >= i64::MAX as f64 {
86 return Err(DogeError::value_error(
87 "that timestamp is outside the representable range",
88 ));
89 }
90 let secs = secs.floor() as i64;
91 let days = secs.div_euclid(SECS_PER_DAY);
92 let time_of_day = secs.rem_euclid(SECS_PER_DAY);
93 let (year, month, day) = civil_from_days(days);
94 let hour = time_of_day / 3600;
95 let minute = (time_of_day % 3600) / 60;
96 let second = time_of_day % 60;
97 Ok(Value::str(format!(
98 "{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}Z"
99 )))
100}
101
102pub fn nap_parse(text: &Value) -> DogeResult {
106 let text = str_arg("nap", "parse", text)?;
107 let secs = parse_iso8601(text).ok_or_else(|| {
108 DogeError::value_error(format!(
109 "cannot read \"{text}\" as a timestamp — expected \"YYYY-MM-DDTHH:MM:SSZ\""
110 ))
111 })?;
112 Ok(Value::Float(secs as f64))
113}
114
115fn parse_iso8601(text: &str) -> Option<i64> {
119 let text = text.strip_suffix('Z').unwrap_or(text);
120 let (date, time) = text.split_once('T')?;
121
122 let mut date_parts = date.splitn(3, '-');
123 let year: i64 = signed_field(date_parts.next()?)?;
124 let month: i64 = field(date_parts.next()?)?;
125 let day: i64 = field(date_parts.next()?)?;
126 if date_parts.next().is_some() {
127 return None;
128 }
129
130 let mut time_parts = time.splitn(3, ':');
131 let hour: i64 = field(time_parts.next()?)?;
132 let minute: i64 = field(time_parts.next()?)?;
133 let second: i64 = field(time_parts.next()?)?;
134 if time_parts.next().is_some() {
135 return None;
136 }
137
138 if !(1..=12).contains(&month)
139 || !(1..=31).contains(&day)
140 || !(0..=23).contains(&hour)
141 || !(0..=59).contains(&minute)
142 || !(0..=59).contains(&second)
143 {
144 return None;
145 }
146
147 let days = days_from_civil(year, month, day);
148 Some(days * SECS_PER_DAY + hour * 3600 + minute * 60 + second)
149}
150
151fn field(s: &str) -> Option<i64> {
154 if s.is_empty() || !s.bytes().all(|b| b.is_ascii_digit()) {
155 return None;
156 }
157 s.parse().ok()
158}
159
160fn signed_field(s: &str) -> Option<i64> {
163 match s.strip_prefix('-') {
164 Some(rest) => field(rest).map(|n| -n),
165 None => field(s),
166 }
167}
168
169fn days_from_civil(year: i64, month: i64, day: i64) -> i64 {
173 let y = if month <= 2 { year - 1 } else { year };
174 let era = if y >= 0 { y } else { y - 399 } / 400;
175 let yoe = y - era * 400;
176 let doy = (153 * (if month > 2 { month - 3 } else { month + 9 }) + 2) / 5 + day - 1;
177 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
178 era * 146_097 + doe - 719_468
179}
180
181fn civil_from_days(days: i64) -> (i64, i64, i64) {
184 let z = days + 719_468;
185 let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
186 let doe = z - era * 146_097;
187 let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
188 let year = yoe + era * 400;
189 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
190 let mp = (5 * doy + 2) / 153;
191 let day = doy - (153 * mp + 2) / 5 + 1;
192 let month = if mp < 10 { mp + 3 } else { mp - 9 };
193 (if month <= 2 { year + 1 } else { year }, month, day)
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199 use crate::error::ErrorKind;
200
201 fn stamp(secs: i64) -> String {
202 match nap_stamp(&Value::int(secs)).unwrap() {
203 Value::Str(s) => s.to_string(),
204 other => panic!("expected a Str, got {other:?}"),
205 }
206 }
207
208 fn parse(text: &str) -> f64 {
209 match nap_parse(&Value::str(text)).unwrap() {
210 Value::Float(f) => f,
211 other => panic!("expected a Float, got {other:?}"),
212 }
213 }
214
215 #[test]
216 fn stamp_formats_known_timestamps() {
217 assert_eq!(stamp(0), "1970-01-01T00:00:00Z");
218 assert_eq!(stamp(946_684_800), "2000-01-01T00:00:00Z");
219 assert_eq!(stamp(1_609_459_199), "2020-12-31T23:59:59Z");
220 }
221
222 #[test]
223 fn stamp_handles_pre_epoch_dates() {
224 assert_eq!(stamp(-1), "1969-12-31T23:59:59Z");
225 assert_eq!(stamp(-SECS_PER_DAY), "1969-12-31T00:00:00Z");
226 }
227
228 #[test]
229 fn parse_is_the_inverse_of_stamp() {
230 for secs in [0, 946_684_800, 1_609_459_199, -1, -SECS_PER_DAY] {
231 assert_eq!(parse(&stamp(secs)), secs as f64);
232 }
233 }
234
235 #[test]
236 fn parse_tolerates_a_missing_z() {
237 assert_eq!(parse("2000-01-01T00:00:00"), 946_684_800.0);
238 }
239
240 #[test]
241 fn parse_rejects_malformed_input() {
242 for bad in [
243 "not a date",
244 "2000-13-01T00:00:00Z",
245 "2000-01-32T00:00:00Z",
246 "2000-01-01T24:00:00Z",
247 "2000-01-01 00:00:00Z",
248 "2000/01/01T00:00:00Z",
249 "2000-01-01T00:00:00+02:00",
250 "",
251 ] {
252 assert_eq!(
253 nap_parse(&Value::str(bad)).unwrap_err().kind,
254 ErrorKind::ValueError,
255 "{bad:?} should be a ValueError"
256 );
257 }
258 }
259
260 #[test]
261 fn rest_rejects_bad_durations() {
262 assert_eq!(
263 nap_rest(&Value::int(-1)).unwrap_err().kind,
264 ErrorKind::ValueError
265 );
266 assert_eq!(
267 nap_rest(&Value::Float(f64::NAN)).unwrap_err().kind,
268 ErrorKind::ValueError
269 );
270 assert_eq!(
271 nap_rest(&Value::Float(f64::INFINITY)).unwrap_err().kind,
272 ErrorKind::ValueError
273 );
274 }
275
276 #[test]
277 fn rest_zero_returns_none() {
278 assert!(matches!(nap_rest(&Value::int(0)).unwrap(), Value::None));
279 }
280
281 #[test]
282 fn mono_never_goes_backwards() {
283 let a = match nap_mono().unwrap() {
284 Value::Float(f) => f,
285 other => panic!("expected a Float, got {other:?}"),
286 };
287 let b = match nap_mono().unwrap() {
288 Value::Float(f) => f,
289 other => panic!("expected a Float, got {other:?}"),
290 };
291 assert!(b >= a);
292 }
293
294 #[test]
295 fn now_is_after_the_epoch() {
296 match nap_now().unwrap() {
297 Value::Float(f) => assert!(f > 0.0),
298 other => panic!("expected a Float, got {other:?}"),
299 }
300 }
301
302 #[test]
303 fn stamp_out_of_range_is_value_error() {
304 assert_eq!(
305 nap_stamp(&Value::Float(f64::INFINITY)).unwrap_err().kind,
306 ErrorKind::ValueError
307 );
308 }
309
310 #[test]
311 fn bad_arg_types_are_type_errors() {
312 assert_eq!(
313 nap_rest(&Value::str("soon")).unwrap_err().kind,
314 ErrorKind::TypeError
315 );
316 assert_eq!(
317 nap_stamp(&Value::str("soon")).unwrap_err().kind,
318 ErrorKind::TypeError
319 );
320 assert_eq!(
321 nap_parse(&Value::int(0)).unwrap_err().kind,
322 ErrorKind::TypeError
323 );
324 }
325}