Skip to main content

mrubyedge_time/
lib.rs

1use std::any::Any;
2use std::cell::{Cell, RefCell};
3use std::rc::Rc;
4
5use mrubyedge::{
6    Error,
7    yamrb::{
8        helpers::{mrb_define_class_cmethod, mrb_define_cmethod, mrb_funcall},
9        value::{RData, RHashMap, RObject, RType, RValue},
10        vm::VM,
11    },
12};
13
14/// Type alias for datetime parts: (year, month, day, wday, hour, min, sec)
15type DateTimeParts = (i32, u32, u32, u32, u32, u32, u32);
16
17/// Rust-side representation of a Ruby Time object.
18/// Stores seconds and nanoseconds since UNIX epoch, and UTC offset in seconds.
19#[derive(Debug, Clone)]
20pub struct RTimeData {
21    /// Seconds since UNIX epoch (can be negative for times before 1970)
22    pub sec: i64,
23    /// Nanoseconds within the current second (0..999_999_999)
24    pub nsec: u32,
25    /// UTC offset in seconds (e.g. +9h = 32400, -5h = -18000)
26    pub utc_offset: i32,
27    /// Cached result of to_datetime_parts() — computed lazily, interior-mutable.
28    cached_parts: Cell<Option<DateTimeParts>>,
29}
30
31impl RTimeData {
32    pub fn new(sec: i64, nsec: u32, utc_offset: i32) -> Self {
33        RTimeData {
34            sec,
35            nsec,
36            utc_offset,
37            cached_parts: Cell::new(None),
38        }
39    }
40
41    /// Calculate the "local" seconds (sec + utc_offset) for date/time decomposition.
42    fn local_sec(&self) -> i64 {
43        self.sec + self.utc_offset as i64
44    }
45
46    /// Decompose into (year, month, day, wday, hour, min, sec_in_day).
47    /// Uses the proleptic Gregorian calendar algorithm.
48    /// Result is cached on first call via interior mutability.
49    pub fn to_datetime_parts(&self) -> DateTimeParts {
50        if let Some(parts) = self.cached_parts.get() {
51            return parts;
52        }
53        let local = self.local_sec();
54
55        // Time of day
56        let sec_in_day = local.rem_euclid(86400) as u32;
57        let hour = sec_in_day / 3600;
58        let min = (sec_in_day % 3600) / 60;
59        let sec = sec_in_day % 60;
60
61        // Day number from epoch (days since 1970-01-01, can be negative)
62        let days_from_epoch = local.div_euclid(86400);
63
64        // Convert to Julian Day Number; 1970-01-01 = JDN 2440588
65        let jdn = days_from_epoch + 2440588;
66
67        // Gregorian calendar conversion from JDN
68        // Algorithm from: https://en.wikipedia.org/wiki/Julian_day#Julian_day_number_calculation
69        let l = jdn + 68569;
70        let n = (4 * l) / 146097;
71        let l = l - (146097 * n + 3) / 4;
72        let i = (4000 * (l + 1)) / 1461001;
73        let l = l - (1461 * i) / 4 + 31;
74        let j = (80 * l) / 2447;
75        let day = l - (2447 * j) / 80;
76        let l = j / 11;
77        let month = j + 2 - 12 * l;
78        let year = 100 * (n - 49) + i + l;
79
80        // Weekday: JDN mod 7; JDN=0 is Monday in proleptic... actually
81        // 2440588 % 7 = 4, and 1970-01-01 was Thursday (wday=4 in Ruby)
82        let wday = (jdn + 1).rem_euclid(7) as u32; // 0=Sunday, 1=Monday, ...
83
84        let parts = (year as i32, month as u32, day as u32, wday, hour, min, sec);
85        self.cached_parts.set(Some(parts));
86        parts
87    }
88
89    /// Format as "%Y-%m-%d %H:%M:%S %z"
90    pub fn to_s(&self) -> String {
91        let (year, month, day, _wday, hour, min, sec) = self.to_datetime_parts();
92        let offset_sign = if self.utc_offset >= 0 { '+' } else { '-' };
93        let abs_offset = self.utc_offset.unsigned_abs();
94        let offset_h = abs_offset / 3600;
95        let offset_m = (abs_offset % 3600) / 60;
96        format!(
97            "{:04}-{:02}-{:02} {:02}:{:02}:{:02} {}{:02}{:02}",
98            year, month, day, hour, min, sec, offset_sign, offset_h, offset_m
99        )
100    }
101}
102
103/// Extract RTimeData from an RObject (must be a Data object holding RTimeData).
104fn get_time_data(obj: &Rc<RObject>) -> Result<RTimeData, Error> {
105    match &obj.value {
106        RValue::Data(data) => {
107            let borrow = data.data.borrow();
108            let any_ref = borrow
109                .as_ref()
110                .ok_or_else(|| Error::RuntimeError("Invalid Time data".to_string()))?;
111            let time = any_ref
112                .downcast_ref::<RTimeData>()
113                .ok_or_else(|| Error::RuntimeError("Invalid Time data".to_string()))?;
114            Ok(time.clone())
115        }
116        _ => Err(Error::RuntimeError("Expected a Time object".to_string())),
117    }
118}
119
120/// Create an Rc<RObject> wrapping an RTimeData.
121fn make_time_object(vm: &mut VM, time_data: RTimeData) -> Rc<RObject> {
122    let time_class_obj = vm
123        .get_const_by_name("Time")
124        .expect("Time class not found; did you call init_time?");
125    let class = match &time_class_obj.value {
126        RValue::Class(c) => c.clone(),
127        _ => panic!("Time is not a class"),
128    };
129    let rdata = Rc::new(RData {
130        class,
131        data: RefCell::new(Some(Rc::new(Box::new(time_data) as Box<dyn Any>))),
132        ref_count: 1,
133    });
134    Rc::new(RObject {
135        tt: RType::Data,
136        value: RValue::Data(rdata),
137        object_id: Cell::new(u64::MAX),
138        singleton_class: RefCell::new(None),
139        ivar: RefCell::new(RHashMap::default()),
140    })
141}
142
143// ---------------------------------------------------------------------------
144// Class methods
145// ---------------------------------------------------------------------------
146
147/// Time.now
148/// Calls Time.__source to get [sec, nsec], then creates a Time object.
149/// utc_offset defaults to 0 (UTC).
150fn mrb_time_now(vm: &mut VM, _args: &[Rc<RObject>]) -> Result<Rc<RObject>, Error> {
151    let time_class_obj = vm
152        .get_const_by_name("Time")
153        .ok_or_else(|| Error::RuntimeError("Time class not found".to_string()))?;
154
155    // Call Time.__source -> [sec, nsec]
156    let source = mrb_funcall(vm, Some(time_class_obj), "__source", &[])?;
157    let (sec, nsec) = source.as_ref().try_into()?;
158
159    Ok(make_time_object(vm, RTimeData::new(sec, nsec, 0)))
160}
161
162/// Time.at(sec) or Time.at(sec, nsec)
163fn mrb_time_at(vm: &mut VM, args: &[Rc<RObject>]) -> Result<Rc<RObject>, Error> {
164    let args = strip_trailing_nil(args);
165    if args.is_empty() {
166        return Err(Error::ArgumentError(
167            "wrong number of arguments (given 0, expected 1+)".to_string(),
168        ));
169    }
170
171    let sec = get_integer_or_float_as_i64(&args[0])?;
172    let nsec = if args.len() >= 2 {
173        get_integer_or_float_as_u32(&args[1])?
174    } else {
175        0
176    };
177
178    Ok(make_time_object(vm, RTimeData::new(sec, nsec, 0)))
179}
180
181// ---------------------------------------------------------------------------
182// Instance methods
183// ---------------------------------------------------------------------------
184
185/// Time#year
186fn mrb_time_year(vm: &mut VM, _args: &[Rc<RObject>]) -> Result<Rc<RObject>, Error> {
187    let self_obj = vm.getself()?;
188    let t = get_time_data(&self_obj)?;
189    let (year, _, _, _, _, _, _) = t.to_datetime_parts();
190    Ok(RObject::integer(year as i64).to_refcount_assigned())
191}
192
193/// Time#month (alias: mon)
194fn mrb_time_month(vm: &mut VM, _args: &[Rc<RObject>]) -> Result<Rc<RObject>, Error> {
195    let self_obj = vm.getself()?;
196    let t = get_time_data(&self_obj)?;
197    let (_, month, _, _, _, _, _) = t.to_datetime_parts();
198    Ok(RObject::integer(month as i64).to_refcount_assigned())
199}
200
201/// Time#day (alias: mday)
202fn mrb_time_day(vm: &mut VM, _args: &[Rc<RObject>]) -> Result<Rc<RObject>, Error> {
203    let self_obj = vm.getself()?;
204    let t = get_time_data(&self_obj)?;
205    let (_, _, day, _, _, _, _) = t.to_datetime_parts();
206    Ok(RObject::integer(day as i64).to_refcount_assigned())
207}
208
209/// Time#wday (0=Sunday, 1=Monday, ..., 6=Saturday)
210fn mrb_time_wday(vm: &mut VM, _args: &[Rc<RObject>]) -> Result<Rc<RObject>, Error> {
211    let self_obj = vm.getself()?;
212    let t = get_time_data(&self_obj)?;
213    let (_, _, _, wday, _, _, _) = t.to_datetime_parts();
214    Ok(RObject::integer(wday as i64).to_refcount_assigned())
215}
216
217/// Time#hour
218fn mrb_time_hour(vm: &mut VM, _args: &[Rc<RObject>]) -> Result<Rc<RObject>, Error> {
219    let self_obj = vm.getself()?;
220    let t = get_time_data(&self_obj)?;
221    let (_, _, _, _, hour, _, _) = t.to_datetime_parts();
222    Ok(RObject::integer(hour as i64).to_refcount_assigned())
223}
224
225/// Time#min
226fn mrb_time_min(vm: &mut VM, _args: &[Rc<RObject>]) -> Result<Rc<RObject>, Error> {
227    let self_obj = vm.getself()?;
228    let t = get_time_data(&self_obj)?;
229    let (_, _, _, _, _, min, _) = t.to_datetime_parts();
230    Ok(RObject::integer(min as i64).to_refcount_assigned())
231}
232
233/// Time#sec
234fn mrb_time_sec(vm: &mut VM, _args: &[Rc<RObject>]) -> Result<Rc<RObject>, Error> {
235    let self_obj = vm.getself()?;
236    let t = get_time_data(&self_obj)?;
237    let (_, _, _, _, _, _, sec) = t.to_datetime_parts();
238    Ok(RObject::integer(sec as i64).to_refcount_assigned())
239}
240
241/// Time#nsec
242fn mrb_time_nsec(vm: &mut VM, _args: &[Rc<RObject>]) -> Result<Rc<RObject>, Error> {
243    let self_obj = vm.getself()?;
244    let t = get_time_data(&self_obj)?;
245    Ok(RObject::integer(t.nsec as i64).to_refcount_assigned())
246}
247
248/// Time#to_s -> "%Y-%m-%d %H:%M:%S %z"
249fn mrb_time_to_s(vm: &mut VM, _args: &[Rc<RObject>]) -> Result<Rc<RObject>, Error> {
250    let self_obj = vm.getself()?;
251    let t = get_time_data(&self_obj)?;
252    Ok(RObject::string(t.to_s()).to_refcount_assigned())
253}
254
255/// Time#+ (sec as integer or float)
256fn mrb_time_add(vm: &mut VM, args: &[Rc<RObject>]) -> Result<Rc<RObject>, Error> {
257    let args = strip_trailing_nil(args);
258    if args.is_empty() {
259        return Err(Error::ArgumentError(
260            "wrong number of arguments (given 0, expected 1)".to_string(),
261        ));
262    }
263    let self_obj = vm.getself()?;
264    let t = get_time_data(&self_obj)?;
265
266    let (delta_sec, delta_nsec) = float_to_sec_nsec(&args[0])?;
267    let new_nsec = t.nsec as i64 + delta_nsec as i64;
268    let carry = new_nsec.div_euclid(1_000_000_000);
269    let new_nsec = new_nsec.rem_euclid(1_000_000_000) as u32;
270    let new_sec = t.sec + delta_sec + carry;
271
272    Ok(make_time_object(
273        vm,
274        RTimeData::new(new_sec, new_nsec, t.utc_offset),
275    ))
276}
277
278/// Time#- (sec as integer or float), also supports Time - Time -> Float (seconds)
279fn mrb_time_sub(vm: &mut VM, args: &[Rc<RObject>]) -> Result<Rc<RObject>, Error> {
280    let args = strip_trailing_nil(args);
281    if args.is_empty() {
282        return Err(Error::ArgumentError(
283            "wrong number of arguments (given 0, expected 1)".to_string(),
284        ));
285    }
286    let self_obj = vm.getself()?;
287    let t = get_time_data(&self_obj)?;
288
289    // Check if rhs is a Time object
290    if let RValue::Data(_) = &args[0].value
291        && let Ok(rhs) = get_time_data(&args[0])
292    {
293        // Time - Time -> Float (difference in seconds)
294        let sec_diff =
295            (t.sec - rhs.sec) as f64 + (t.nsec as f64 - rhs.nsec as f64) / 1_000_000_000.0;
296        return Ok(RObject::float(sec_diff).to_refcount_assigned());
297    }
298
299    let (delta_sec, delta_nsec) = float_to_sec_nsec(&args[0])?;
300    let new_nsec = t.nsec as i64 - delta_nsec as i64;
301    let carry = new_nsec.div_euclid(1_000_000_000);
302    let new_nsec = new_nsec.rem_euclid(1_000_000_000) as u32;
303    let new_sec = t.sec - delta_sec + carry;
304
305    Ok(make_time_object(
306        vm,
307        RTimeData::new(new_sec, new_nsec, t.utc_offset),
308    ))
309}
310
311/// Time#<=> (compare with another Time object)
312fn mrb_time_cmp(vm: &mut VM, args: &[Rc<RObject>]) -> Result<Rc<RObject>, Error> {
313    let args = strip_trailing_nil(args);
314    if args.is_empty() {
315        return Err(Error::ArgumentError(
316            "wrong number of arguments (given 0, expected 1)".to_string(),
317        ));
318    }
319    let self_obj = vm.getself()?;
320    let t = get_time_data(&self_obj)?;
321
322    let rhs = match get_time_data(&args[0]) {
323        Ok(r) => r,
324        Err(_) => return Ok(RObject::nil().to_refcount_assigned()),
325    };
326
327    let result = match t.sec.cmp(&rhs.sec) {
328        std::cmp::Ordering::Equal => t.nsec.cmp(&rhs.nsec),
329        other => other,
330    };
331
332    let int_val = match result {
333        std::cmp::Ordering::Less => -1i64,
334        std::cmp::Ordering::Equal => 0,
335        std::cmp::Ordering::Greater => 1,
336    };
337    Ok(RObject::integer(int_val).to_refcount_assigned())
338}
339
340/// Time#utc_offset -> Integer (seconds)
341fn mrb_time_utc_offset(vm: &mut VM, _args: &[Rc<RObject>]) -> Result<Rc<RObject>, Error> {
342    let self_obj = vm.getself()?;
343    let t = get_time_data(&self_obj)?;
344    Ok(RObject::integer(t.utc_offset as i64).to_refcount_assigned())
345}
346
347/// Time#localtime(offset) - returns a new Time with the given UTC offset (in seconds)
348fn mrb_time_localtime(vm: &mut VM, args: &[Rc<RObject>]) -> Result<Rc<RObject>, Error> {
349    let args = strip_trailing_nil(args);
350    let self_obj = vm.getself()?;
351    let t = get_time_data(&self_obj)?;
352
353    let new_offset = if args.is_empty() {
354        0i32 // default to UTC if no arg
355    } else {
356        get_integer_or_float_as_i64(&args[0])? as i32
357    };
358
359    Ok(make_time_object(
360        vm,
361        RTimeData::new(t.sec, t.nsec, new_offset),
362    ))
363}
364
365/// Time#to_i -> sec
366fn mrb_time_to_i(vm: &mut VM, _args: &[Rc<RObject>]) -> Result<Rc<RObject>, Error> {
367    let self_obj = vm.getself()?;
368    let t = get_time_data(&self_obj)?;
369    Ok(RObject::integer(t.sec).to_refcount_assigned())
370}
371
372/// Time#to_f -> sec.nsec as float
373fn mrb_time_to_f(vm: &mut VM, _args: &[Rc<RObject>]) -> Result<Rc<RObject>, Error> {
374    let self_obj = vm.getself()?;
375    let t = get_time_data(&self_obj)?;
376    let f = t.sec as f64 + t.nsec as f64 / 1_000_000_000.0;
377    Ok(RObject::float(f).to_refcount_assigned())
378}
379
380// ---------------------------------------------------------------------------
381// Default Time.__source (std::time based, non-wasm)
382// ---------------------------------------------------------------------------
383
384/// Default implementation of Time.__source using std::time.
385/// Returns [sec, nsec] as a Ruby array.
386/// Compiled on non-wasm targets, and also on wasm32-wasi where std::time is available.
387#[cfg(any(not(target_arch = "wasm32"), target_os = "wasi"))]
388fn mrb_time_source_default(_vm: &mut VM, _args: &[Rc<RObject>]) -> Result<Rc<RObject>, Error> {
389    use std::time::{SystemTime, UNIX_EPOCH};
390    let now = SystemTime::now();
391    let unixtime = now.duration_since(UNIX_EPOCH).map_err(|_| {
392        Error::RuntimeError(
393            "system time before UNIX EPOCH -- are you running this from the past?".to_string(),
394        )
395    })?;
396    let sec = unixtime.as_secs() as i64;
397    let nsec = unixtime.subsec_nanos() as i64;
398    let arr = vec![
399        RObject::integer(sec).to_refcount_assigned(),
400        RObject::integer(nsec).to_refcount_assigned(),
401    ];
402    Ok(RObject::array(arr).to_refcount_assigned())
403}
404
405// ---------------------------------------------------------------------------
406// Helper utilities
407// ---------------------------------------------------------------------------
408
409fn strip_trailing_nil(args: &[Rc<RObject>]) -> &[Rc<RObject>] {
410    if !args.is_empty() && args[args.len() - 1].is_nil() {
411        &args[0..args.len() - 1]
412    } else {
413        args
414    }
415}
416
417fn get_integer_or_float_as_i64(obj: &RObject) -> Result<i64, Error> {
418    match &obj.value {
419        RValue::Integer(i) => Ok(*i),
420        RValue::Float(f) => Ok(*f as i64),
421        _ => Err(Error::ArgumentError(
422            "expected Integer or Float".to_string(),
423        )),
424    }
425}
426
427fn get_integer_or_float_as_u32(obj: &RObject) -> Result<u32, Error> {
428    match &obj.value {
429        RValue::Integer(i) => {
430            if *i < 0 {
431                return Err(Error::ArgumentError(
432                    "nsec must be non-negative".to_string(),
433                ));
434            }
435            Ok(*i as u32)
436        }
437        RValue::Float(f) => {
438            if *f < 0.0 {
439                return Err(Error::ArgumentError(
440                    "nsec must be non-negative".to_string(),
441                ));
442            }
443            Ok(*f as u32)
444        }
445        _ => Err(Error::ArgumentError(
446            "expected Integer or Float".to_string(),
447        )),
448    }
449}
450
451/// Convert a numeric seconds value (possibly fractional) to (whole_sec, nsec).
452fn float_to_sec_nsec(obj: &RObject) -> Result<(i64, u32), Error> {
453    match &obj.value {
454        RValue::Integer(i) => Ok((*i, 0)),
455        RValue::Float(f) => {
456            let sec = f.trunc() as i64;
457            let nsec = (f.fract().abs() * 1_000_000_000.0).round() as u32;
458            Ok((sec, nsec))
459        }
460        _ => Err(Error::ArgumentError(
461            "expected Integer or Float".to_string(),
462        )),
463    }
464}
465
466// ---------------------------------------------------------------------------
467// Public initializer
468// ---------------------------------------------------------------------------
469
470/// Initialize the Time class in the VM.
471/// Call this after `VM::open` to make `Time` available in Ruby code.
472pub fn init_time(vm: &mut VM) {
473    let time_class = vm.define_class("Time", None, None);
474
475    // Class methods
476    mrb_define_class_cmethod(vm, time_class.clone(), "now", Box::new(mrb_time_now));
477    mrb_define_class_cmethod(vm, time_class.clone(), "at", Box::new(mrb_time_at));
478
479    // Instance methods
480    mrb_define_cmethod(vm, time_class.clone(), "year", Box::new(mrb_time_year));
481    mrb_define_cmethod(vm, time_class.clone(), "month", Box::new(mrb_time_month));
482    mrb_define_cmethod(vm, time_class.clone(), "mon", Box::new(mrb_time_month));
483    mrb_define_cmethod(vm, time_class.clone(), "day", Box::new(mrb_time_day));
484    mrb_define_cmethod(vm, time_class.clone(), "mday", Box::new(mrb_time_day));
485    mrb_define_cmethod(vm, time_class.clone(), "wday", Box::new(mrb_time_wday));
486    mrb_define_cmethod(vm, time_class.clone(), "hour", Box::new(mrb_time_hour));
487    mrb_define_cmethod(vm, time_class.clone(), "min", Box::new(mrb_time_min));
488    mrb_define_cmethod(vm, time_class.clone(), "sec", Box::new(mrb_time_sec));
489    mrb_define_cmethod(vm, time_class.clone(), "nsec", Box::new(mrb_time_nsec));
490    mrb_define_cmethod(vm, time_class.clone(), "to_s", Box::new(mrb_time_to_s));
491    mrb_define_cmethod(vm, time_class.clone(), "inspect", Box::new(mrb_time_to_s));
492    mrb_define_cmethod(vm, time_class.clone(), "+", Box::new(mrb_time_add));
493    mrb_define_cmethod(vm, time_class.clone(), "-", Box::new(mrb_time_sub));
494    mrb_define_cmethod(vm, time_class.clone(), "<=>", Box::new(mrb_time_cmp));
495    mrb_define_cmethod(
496        vm,
497        time_class.clone(),
498        "utc_offset",
499        Box::new(mrb_time_utc_offset),
500    );
501    mrb_define_cmethod(
502        vm,
503        time_class.clone(),
504        "gmt_offset",
505        Box::new(mrb_time_utc_offset),
506    );
507    mrb_define_cmethod(
508        vm,
509        time_class.clone(),
510        "localtime",
511        Box::new(mrb_time_localtime),
512    );
513    mrb_define_cmethod(vm, time_class.clone(), "to_i", Box::new(mrb_time_to_i));
514    mrb_define_cmethod(vm, time_class.clone(), "to_f", Box::new(mrb_time_to_f));
515
516    // Register default Time.__source on non-wasm targets, and also on wasm32-wasi
517    #[cfg(any(not(target_arch = "wasm32"), target_os = "wasi"))]
518    {
519        let _time_class_obj = RObject::class(time_class, vm);
520        let time_class_obj_for_source = vm
521            .get_const_by_name("Time")
522            .expect("Time class not found after definition");
523        mrb_define_class_cmethod_on_obj(
524            vm,
525            time_class_obj_for_source,
526            "__source",
527            Box::new(mrb_time_source_default),
528        );
529    }
530}
531
532/// Helper: define a singleton (class-side) cmethod on a class RObject.
533#[cfg(any(not(target_arch = "wasm32"), target_os = "wasi"))]
534fn mrb_define_class_cmethod_on_obj(
535    vm: &mut VM,
536    class_obj: Rc<RObject>,
537    name: &str,
538    cmethod: mrubyedge::yamrb::value::RFn,
539) {
540    use mrubyedge::yamrb::helpers::mrb_define_singleton_cmethod;
541    mrb_define_singleton_cmethod(vm, class_obj, name, cmethod);
542}