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, utc_offset], then creates a Time object.
149fn mrb_time_now(vm: &mut VM, _args: &[Rc<RObject>]) -> Result<Rc<RObject>, Error> {
150    let time_class_obj = vm
151        .get_const_by_name("Time")
152        .ok_or_else(|| Error::RuntimeError("Time class not found".to_string()))?;
153
154    // Call Time.__source -> [sec, nsec, utc_offset]
155    let source = mrb_funcall(vm, Some(time_class_obj), "__source", &[])?;
156    let (sec, nsec, utc_offset) = source.as_ref().try_into()?;
157
158    Ok(make_time_object(vm, RTimeData::new(sec, nsec, utc_offset)))
159}
160
161/// Time.at(sec) or Time.at(sec, nsec)
162fn mrb_time_at(vm: &mut VM, args: &[Rc<RObject>]) -> Result<Rc<RObject>, Error> {
163    if args.is_empty() {
164        return Err(Error::ArgumentError(
165            "wrong number of arguments (given 0, expected 1+)".to_string(),
166        ));
167    }
168
169    let sec = get_integer_or_float_as_i64(&args[0])?;
170    let nsec = if args.len() >= 2 {
171        get_integer_or_float_as_u32(&args[1])?
172    } else {
173        0
174    };
175
176    Ok(make_time_object(vm, RTimeData::new(sec, nsec, 0)))
177}
178
179// ---------------------------------------------------------------------------
180// Instance methods
181// ---------------------------------------------------------------------------
182
183/// Time#year
184fn mrb_time_year(vm: &mut VM, _args: &[Rc<RObject>]) -> Result<Rc<RObject>, Error> {
185    let self_obj = vm.getself()?;
186    let t = get_time_data(&self_obj)?;
187    let (year, _, _, _, _, _, _) = t.to_datetime_parts();
188    Ok(RObject::integer(year as i64).to_refcount_assigned())
189}
190
191/// Time#month (alias: mon)
192fn mrb_time_month(vm: &mut VM, _args: &[Rc<RObject>]) -> Result<Rc<RObject>, Error> {
193    let self_obj = vm.getself()?;
194    let t = get_time_data(&self_obj)?;
195    let (_, month, _, _, _, _, _) = t.to_datetime_parts();
196    Ok(RObject::integer(month as i64).to_refcount_assigned())
197}
198
199/// Time#day (alias: mday)
200fn mrb_time_day(vm: &mut VM, _args: &[Rc<RObject>]) -> Result<Rc<RObject>, Error> {
201    let self_obj = vm.getself()?;
202    let t = get_time_data(&self_obj)?;
203    let (_, _, day, _, _, _, _) = t.to_datetime_parts();
204    Ok(RObject::integer(day as i64).to_refcount_assigned())
205}
206
207/// Time#wday (0=Sunday, 1=Monday, ..., 6=Saturday)
208fn mrb_time_wday(vm: &mut VM, _args: &[Rc<RObject>]) -> Result<Rc<RObject>, Error> {
209    let self_obj = vm.getself()?;
210    let t = get_time_data(&self_obj)?;
211    let (_, _, _, wday, _, _, _) = t.to_datetime_parts();
212    Ok(RObject::integer(wday as i64).to_refcount_assigned())
213}
214
215/// Time#hour
216fn mrb_time_hour(vm: &mut VM, _args: &[Rc<RObject>]) -> Result<Rc<RObject>, Error> {
217    let self_obj = vm.getself()?;
218    let t = get_time_data(&self_obj)?;
219    let (_, _, _, _, hour, _, _) = t.to_datetime_parts();
220    Ok(RObject::integer(hour as i64).to_refcount_assigned())
221}
222
223/// Time#min
224fn mrb_time_min(vm: &mut VM, _args: &[Rc<RObject>]) -> Result<Rc<RObject>, Error> {
225    let self_obj = vm.getself()?;
226    let t = get_time_data(&self_obj)?;
227    let (_, _, _, _, _, min, _) = t.to_datetime_parts();
228    Ok(RObject::integer(min as i64).to_refcount_assigned())
229}
230
231/// Time#sec
232fn mrb_time_sec(vm: &mut VM, _args: &[Rc<RObject>]) -> Result<Rc<RObject>, Error> {
233    let self_obj = vm.getself()?;
234    let t = get_time_data(&self_obj)?;
235    let (_, _, _, _, _, _, sec) = t.to_datetime_parts();
236    Ok(RObject::integer(sec as i64).to_refcount_assigned())
237}
238
239/// Time#nsec
240fn mrb_time_nsec(vm: &mut VM, _args: &[Rc<RObject>]) -> Result<Rc<RObject>, Error> {
241    let self_obj = vm.getself()?;
242    let t = get_time_data(&self_obj)?;
243    Ok(RObject::integer(t.nsec as i64).to_refcount_assigned())
244}
245
246/// Time#to_s -> "%Y-%m-%d %H:%M:%S %z"
247fn mrb_time_to_s(vm: &mut VM, _args: &[Rc<RObject>]) -> Result<Rc<RObject>, Error> {
248    let self_obj = vm.getself()?;
249    let t = get_time_data(&self_obj)?;
250    Ok(RObject::string(t.to_s()).to_refcount_assigned())
251}
252
253/// Time#+ (sec as integer or float)
254fn mrb_time_add(vm: &mut VM, args: &[Rc<RObject>]) -> Result<Rc<RObject>, Error> {
255    if args.is_empty() {
256        return Err(Error::ArgumentError(
257            "wrong number of arguments (given 0, expected 1)".to_string(),
258        ));
259    }
260    let self_obj = vm.getself()?;
261    let t = get_time_data(&self_obj)?;
262
263    let (delta_sec, delta_nsec) = float_to_sec_nsec(&args[0])?;
264    let new_nsec = t.nsec as i64 + delta_nsec as i64;
265    let carry = new_nsec.div_euclid(1_000_000_000);
266    let new_nsec = new_nsec.rem_euclid(1_000_000_000) as u32;
267    let new_sec = t.sec + delta_sec + carry;
268
269    Ok(make_time_object(
270        vm,
271        RTimeData::new(new_sec, new_nsec, t.utc_offset),
272    ))
273}
274
275/// Time#- (sec as integer or float), also supports Time - Time -> Float (seconds)
276fn mrb_time_sub(vm: &mut VM, args: &[Rc<RObject>]) -> Result<Rc<RObject>, Error> {
277    if args.is_empty() {
278        return Err(Error::ArgumentError(
279            "wrong number of arguments (given 0, expected 1)".to_string(),
280        ));
281    }
282    let self_obj = vm.getself()?;
283    let t = get_time_data(&self_obj)?;
284
285    // Check if rhs is a Time object
286    if let RValue::Data(_) = &args[0].value
287        && let Ok(rhs) = get_time_data(&args[0])
288    {
289        // Time - Time -> Float (difference in seconds)
290        let sec_diff =
291            (t.sec - rhs.sec) as f64 + (t.nsec as f64 - rhs.nsec as f64) / 1_000_000_000.0;
292        return Ok(RObject::float(sec_diff).to_refcount_assigned());
293    }
294
295    let (delta_sec, delta_nsec) = float_to_sec_nsec(&args[0])?;
296    let new_nsec = t.nsec as i64 - delta_nsec as i64;
297    let carry = new_nsec.div_euclid(1_000_000_000);
298    let new_nsec = new_nsec.rem_euclid(1_000_000_000) as u32;
299    let new_sec = t.sec - delta_sec + carry;
300
301    Ok(make_time_object(
302        vm,
303        RTimeData::new(new_sec, new_nsec, t.utc_offset),
304    ))
305}
306
307/// Time#<=> (compare with another Time object)
308fn mrb_time_cmp(vm: &mut VM, args: &[Rc<RObject>]) -> Result<Rc<RObject>, Error> {
309    if args.is_empty() {
310        return Err(Error::ArgumentError(
311            "wrong number of arguments (given 0, expected 1)".to_string(),
312        ));
313    }
314    let self_obj = vm.getself()?;
315    let t = get_time_data(&self_obj)?;
316
317    let rhs = match get_time_data(&args[0]) {
318        Ok(r) => r,
319        Err(_) => return Ok(RObject::nil().to_refcount_assigned()),
320    };
321
322    let result = match t.sec.cmp(&rhs.sec) {
323        std::cmp::Ordering::Equal => t.nsec.cmp(&rhs.nsec),
324        other => other,
325    };
326
327    let int_val = match result {
328        std::cmp::Ordering::Less => -1i64,
329        std::cmp::Ordering::Equal => 0,
330        std::cmp::Ordering::Greater => 1,
331    };
332    Ok(RObject::integer(int_val).to_refcount_assigned())
333}
334
335/// Time#utc_offset -> Integer (seconds)
336fn mrb_time_utc_offset(vm: &mut VM, _args: &[Rc<RObject>]) -> Result<Rc<RObject>, Error> {
337    let self_obj = vm.getself()?;
338    let t = get_time_data(&self_obj)?;
339    Ok(RObject::integer(t.utc_offset as i64).to_refcount_assigned())
340}
341
342/// Time#localtime(offset) - returns a new Time with the given UTC offset (in seconds)
343fn mrb_time_localtime(vm: &mut VM, args: &[Rc<RObject>]) -> Result<Rc<RObject>, Error> {
344    let self_obj = vm.getself()?;
345    let t = get_time_data(&self_obj)?;
346
347    let new_offset = if args.is_empty() {
348        0i32 // default to UTC if no arg
349    } else {
350        get_integer_or_float_as_i64(&args[0])? as i32
351    };
352
353    Ok(make_time_object(
354        vm,
355        RTimeData::new(t.sec, t.nsec, new_offset),
356    ))
357}
358
359/// Time#to_i -> sec
360fn mrb_time_to_i(vm: &mut VM, _args: &[Rc<RObject>]) -> Result<Rc<RObject>, Error> {
361    let self_obj = vm.getself()?;
362    let t = get_time_data(&self_obj)?;
363    Ok(RObject::integer(t.sec).to_refcount_assigned())
364}
365
366/// Time#to_f -> sec.nsec as float
367fn mrb_time_to_f(vm: &mut VM, _args: &[Rc<RObject>]) -> Result<Rc<RObject>, Error> {
368    let self_obj = vm.getself()?;
369    let t = get_time_data(&self_obj)?;
370    let f = t.sec as f64 + t.nsec as f64 / 1_000_000_000.0;
371    Ok(RObject::float(f).to_refcount_assigned())
372}
373
374// ---------------------------------------------------------------------------
375// Default Time.__source (std::time based, non-wasm)
376// ---------------------------------------------------------------------------
377
378/// Default implementation of Time.__source using std::time.
379/// Returns [sec, nsec, utc_offset] as a Ruby array.
380/// utc_offset is the local timezone offset in seconds (e.g. JST = +32400).
381/// Compiled on non-wasm targets, and also on wasm32-wasi where std::time is available.
382#[cfg(any(not(target_arch = "wasm32"), target_os = "wasi"))]
383fn mrb_time_source_default(_vm: &mut VM, _args: &[Rc<RObject>]) -> Result<Rc<RObject>, Error> {
384    use std::time::{SystemTime, UNIX_EPOCH};
385    let now = SystemTime::now();
386    let unixtime = now.duration_since(UNIX_EPOCH).map_err(|_| {
387        Error::RuntimeError(
388            "system time before UNIX EPOCH -- are you running this from the past?".to_string(),
389        )
390    })?;
391    let sec = unixtime.as_secs() as i64;
392    let nsec = unixtime.subsec_nanos() as i64;
393    let utc_offset = local_utc_offset_secs();
394    let arr = vec![
395        RObject::integer(sec).to_refcount_assigned(),
396        RObject::integer(nsec).to_refcount_assigned(),
397        RObject::integer(utc_offset as i64).to_refcount_assigned(),
398    ];
399    Ok(RObject::array(arr).to_refcount_assigned())
400}
401
402/// Return the local timezone UTC offset in seconds using POSIX localtime_r.
403/// Positive values are east of UTC (e.g. JST = +32400).
404#[cfg(any(not(target_arch = "wasm32"), target_os = "wasi"))]
405fn local_utc_offset_secs() -> i32 {
406    use std::time::{SystemTime, UNIX_EPOCH};
407    let t = SystemTime::now()
408        .duration_since(UNIX_EPOCH)
409        .map(|d| d.as_secs() as libc::time_t)
410        .unwrap_or(0);
411    unsafe {
412        let mut tm: libc::tm = std::mem::zeroed();
413        libc::localtime_r(&t, &mut tm);
414        tm.tm_gmtoff as i32
415    }
416}
417
418// ---------------------------------------------------------------------------
419// Helper utilities
420// ---------------------------------------------------------------------------
421
422fn get_integer_or_float_as_i64(obj: &RObject) -> Result<i64, Error> {
423    match &obj.value {
424        RValue::Integer(i) => Ok(*i),
425        RValue::Float(f) => Ok(*f as i64),
426        _ => Err(Error::ArgumentError(
427            "expected Integer or Float".to_string(),
428        )),
429    }
430}
431
432fn get_integer_or_float_as_u32(obj: &RObject) -> Result<u32, Error> {
433    match &obj.value {
434        RValue::Integer(i) => {
435            if *i < 0 {
436                return Err(Error::ArgumentError(
437                    "nsec must be non-negative".to_string(),
438                ));
439            }
440            Ok(*i as u32)
441        }
442        RValue::Float(f) => {
443            if *f < 0.0 {
444                return Err(Error::ArgumentError(
445                    "nsec must be non-negative".to_string(),
446                ));
447            }
448            Ok(*f as u32)
449        }
450        _ => Err(Error::ArgumentError(
451            "expected Integer or Float".to_string(),
452        )),
453    }
454}
455
456/// Convert a numeric seconds value (possibly fractional) to (whole_sec, nsec).
457fn float_to_sec_nsec(obj: &RObject) -> Result<(i64, u32), Error> {
458    match &obj.value {
459        RValue::Integer(i) => Ok((*i, 0)),
460        RValue::Float(f) => {
461            let sec = f.trunc() as i64;
462            let nsec = (f.fract().abs() * 1_000_000_000.0).round() as u32;
463            Ok((sec, nsec))
464        }
465        _ => Err(Error::ArgumentError(
466            "expected Integer or Float".to_string(),
467        )),
468    }
469}
470
471// ---------------------------------------------------------------------------
472// Public initializer
473// ---------------------------------------------------------------------------
474
475/// Initialize the Time class in the VM.
476/// Call this after `VM::open` to make `Time` available in Ruby code.
477pub fn init_time(vm: &mut VM) {
478    let time_class = vm.define_class("Time", None, None);
479
480    // Class methods
481    mrb_define_class_cmethod(vm, time_class.clone(), "now", Box::new(mrb_time_now));
482    mrb_define_class_cmethod(vm, time_class.clone(), "at", Box::new(mrb_time_at));
483
484    // Instance methods
485    mrb_define_cmethod(vm, time_class.clone(), "year", Box::new(mrb_time_year));
486    mrb_define_cmethod(vm, time_class.clone(), "month", Box::new(mrb_time_month));
487    mrb_define_cmethod(vm, time_class.clone(), "mon", Box::new(mrb_time_month));
488    mrb_define_cmethod(vm, time_class.clone(), "day", Box::new(mrb_time_day));
489    mrb_define_cmethod(vm, time_class.clone(), "mday", Box::new(mrb_time_day));
490    mrb_define_cmethod(vm, time_class.clone(), "wday", Box::new(mrb_time_wday));
491    mrb_define_cmethod(vm, time_class.clone(), "hour", Box::new(mrb_time_hour));
492    mrb_define_cmethod(vm, time_class.clone(), "min", Box::new(mrb_time_min));
493    mrb_define_cmethod(vm, time_class.clone(), "sec", Box::new(mrb_time_sec));
494    mrb_define_cmethod(vm, time_class.clone(), "nsec", Box::new(mrb_time_nsec));
495    mrb_define_cmethod(vm, time_class.clone(), "to_s", Box::new(mrb_time_to_s));
496    mrb_define_cmethod(vm, time_class.clone(), "inspect", Box::new(mrb_time_to_s));
497    mrb_define_cmethod(vm, time_class.clone(), "+", Box::new(mrb_time_add));
498    mrb_define_cmethod(vm, time_class.clone(), "-", Box::new(mrb_time_sub));
499    mrb_define_cmethod(vm, time_class.clone(), "<=>", Box::new(mrb_time_cmp));
500    mrb_define_cmethod(
501        vm,
502        time_class.clone(),
503        "utc_offset",
504        Box::new(mrb_time_utc_offset),
505    );
506    mrb_define_cmethod(
507        vm,
508        time_class.clone(),
509        "gmt_offset",
510        Box::new(mrb_time_utc_offset),
511    );
512    mrb_define_cmethod(
513        vm,
514        time_class.clone(),
515        "localtime",
516        Box::new(mrb_time_localtime),
517    );
518    mrb_define_cmethod(vm, time_class.clone(), "to_i", Box::new(mrb_time_to_i));
519    mrb_define_cmethod(vm, time_class.clone(), "to_f", Box::new(mrb_time_to_f));
520
521    // Register default Time.__source on non-wasm targets, and also on wasm32-wasi
522    #[cfg(any(not(target_arch = "wasm32"), target_os = "wasi"))]
523    {
524        let _time_class_obj = RObject::class(time_class, vm);
525        let time_class_obj_for_source = vm
526            .get_const_by_name("Time")
527            .expect("Time class not found after definition");
528        mrb_define_class_cmethod_on_obj(
529            vm,
530            time_class_obj_for_source,
531            "__source",
532            Box::new(mrb_time_source_default),
533        );
534    }
535}
536
537/// Helper: define a singleton (class-side) cmethod on a class RObject.
538#[cfg(any(not(target_arch = "wasm32"), target_os = "wasi"))]
539fn mrb_define_class_cmethod_on_obj(
540    vm: &mut VM,
541    class_obj: Rc<RObject>,
542    name: &str,
543    cmethod: mrubyedge::yamrb::value::RFn,
544) {
545    use mrubyedge::yamrb::helpers::mrb_define_singleton_cmethod;
546    mrb_define_singleton_cmethod(vm, class_obj, name, cmethod);
547}