Skip to main content

endbasic_std/
datetime.rs

1// EndBASIC
2// Copyright 2026 Julio Merino
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU Affero General Public License for more details.
13//
14// You should have received a copy of the GNU Affero General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17//! Time manipulation primitives.
18
19use async_trait::async_trait;
20use endbasic_core::{
21    ArgSepSyntax, CallError, CallResult, Callable, CallableMetadata, CallableMetadataBuilder,
22    ExprType, RequiredValueSyntax, Scope, SingularArgSyntax,
23};
24use std::borrow::Cow;
25use std::rc::Rc;
26use std::thread;
27use std::time::{Duration, Instant};
28
29use crate::MachineBuilder;
30
31/// Category description for all symbols provided by this module.
32pub(crate) const CATEGORY: &str = "Date and time";
33
34/// Interface to date and time functionality provided by the host.
35#[async_trait(?Send)]
36pub trait DateTime {
37    /// Returns the current monotonic timestamp.
38    fn monotonic(&self) -> Duration;
39
40    /// Suspends execution for the given duration.
41    async fn sleep(&self, d: Duration) -> Result<(), String>;
42}
43
44/// Default host-backed implementation of the `DateTime` trait.
45pub struct SystemDateTime {
46    epoch: Instant,
47}
48
49impl Default for SystemDateTime {
50    fn default() -> Self {
51        Self { epoch: Instant::now() }
52    }
53}
54
55#[async_trait(?Send)]
56impl DateTime for SystemDateTime {
57    fn monotonic(&self) -> Duration {
58        self.epoch.elapsed()
59    }
60
61    async fn sleep(&self, d: Duration) -> Result<(), String> {
62        thread::sleep(d);
63        Ok(())
64    }
65}
66
67/// The `MONOTONIC` function.
68pub struct MonotonicFunction {
69    metadata: Rc<CallableMetadata>,
70    datetime: Rc<dyn DateTime>,
71}
72
73impl MonotonicFunction {
74    /// Creates a new instance of the function.
75    pub fn new(datetime: Rc<dyn DateTime>) -> Rc<Self> {
76        Rc::from(Self {
77            metadata: CallableMetadataBuilder::new("MONOTONIC")
78                .with_return_type(ExprType::Double)
79                .with_syntax(&[(&[], None)])
80                .with_category(CATEGORY)
81                .with_description(
82                    "Returns a monotonic timestamp.
83The returned value is the number of seconds since an unspecified reference point.  The timestamp \
84is only useful to compute elapsed time by subtraction and is not related to the current date or \
85wall-clock time.",
86                )
87                .build(),
88            datetime,
89        })
90    }
91}
92
93#[async_trait(?Send)]
94impl Callable for MonotonicFunction {
95    fn metadata(&self) -> Rc<CallableMetadata> {
96        self.metadata.clone()
97    }
98
99    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
100        debug_assert_eq!(0, scope.nargs());
101        scope.return_double(self.datetime.monotonic().as_secs_f64())
102    }
103}
104
105/// The `SLEEP` command.
106pub struct SleepCommand {
107    metadata: Rc<CallableMetadata>,
108    datetime: Rc<dyn DateTime>,
109}
110
111impl SleepCommand {
112    /// Creates a new instance of the command.
113    pub fn new(datetime: Rc<dyn DateTime>) -> Rc<Self> {
114        Rc::from(Self {
115            metadata: CallableMetadataBuilder::new("SLEEP")
116                .with_async(true)
117                .with_syntax(&[(
118                    &[SingularArgSyntax::RequiredValue(
119                        RequiredValueSyntax {
120                            name: Cow::Borrowed("seconds"),
121                            vtype: ExprType::Double,
122                        },
123                        ArgSepSyntax::End,
124                    )],
125                    None,
126                )])
127                .with_category(CATEGORY)
128                .with_description(
129                    "Suspends program execution.
130Pauses program execution for the given number of seconds, which can be specified either as an \
131integer or as a floating point number for finer precision.",
132                )
133                .build(),
134            datetime,
135        })
136    }
137}
138
139#[async_trait(?Send)]
140impl Callable for SleepCommand {
141    fn metadata(&self) -> Rc<CallableMetadata> {
142        self.metadata.clone()
143    }
144
145    async fn async_exec(&self, scope: Scope<'_>) -> CallResult<()> {
146        debug_assert_eq!(1, scope.nargs());
147        let n = scope.get_double(0);
148
149        if n < 0.0 {
150            return Err(CallError::Syntax(
151                scope.get_pos(0),
152                "Sleep time must be positive".to_owned(),
153            ));
154        }
155
156        self.datetime
157            .sleep(Duration::from_secs_f64(n))
158            .await
159            .map_err(|e| CallError::Syntax(scope.get_pos(0), e))
160    }
161}
162
163/// Adds all symbols provided by this module to the given `machine`.
164///
165/// `datetime` provides access to date and time functionality.
166pub fn add_all(machine: &mut MachineBuilder, datetime: Rc<dyn DateTime>) {
167    machine.add_callable(MonotonicFunction::new(datetime.clone()));
168    machine.add_callable(SleepCommand::new(datetime));
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174    use crate::MachineBuilder;
175    use crate::testutils::*;
176    use futures_lite::future::block_on;
177    use std::time::Instant;
178
179    #[test]
180    fn test_monotonic_ok() {
181        let mut tester = Tester::default();
182        tester.get_datetime().add_monotonic(Duration::from_millis(12_345));
183        tester.run("result = MONOTONIC").expect_var("result", 12.345).check();
184    }
185
186    #[test]
187    fn test_monotonic_errors() {
188        check_expr_compilation_error("1:10: MONOTONIC expected no arguments", "MONOTONIC()");
189    }
190
191    #[test]
192    fn test_sleep_ok_int() {
193        let mut tester = Tester::default();
194        tester.run("SLEEP 123").check();
195        assert_eq!(vec![Duration::from_secs(123)], tester.get_datetime().sleeps());
196    }
197
198    #[test]
199    fn test_sleep_ok_float() {
200        let mut tester = Tester::default();
201        tester.run("SLEEP 123.1").check();
202        let sleeps = tester.get_datetime().sleeps();
203        assert_eq!(1, sleeps.len());
204        let ms = sleeps[0].as_millis();
205        assert!(ms > 123095 && ms < 123105, "Bad {}", ms);
206    }
207
208    #[test]
209    fn test_sleep_real() {
210        let before = Instant::now();
211        let mut machine = MachineBuilder::default().build();
212        machine.compile(&mut "SLEEP 0.010".as_bytes()).unwrap();
213        match block_on(machine.exec()) {
214            Ok(None) => (),
215            r => panic!("Expected Ok(None) but got {:?}", r),
216        }
217        assert!(before.elapsed() >= Duration::from_millis(10));
218    }
219
220    #[test]
221    fn test_sleep_errors() {
222        check_stmt_compilation_err("1:1: SLEEP expected seconds#", "SLEEP");
223        check_stmt_compilation_err("1:1: SLEEP expected seconds#", "SLEEP 2, 3");
224        check_stmt_compilation_err("1:1: SLEEP expected seconds#", "SLEEP 2; 3");
225        check_stmt_compilation_err("1:7: STRING is not a number", "SLEEP \"foo\"");
226        check_stmt_err("1:7: Sleep time must be positive", "SLEEP -1");
227        check_stmt_err("1:7: Sleep time must be positive", "SLEEP -0.001");
228    }
229}