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