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