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
14type DateTimeParts = (i32, u32, u32, u32, u32, u32, u32);
16
17#[derive(Debug, Clone)]
20pub struct RTimeData {
21 pub sec: i64,
23 pub nsec: u32,
25 pub utc_offset: i32,
27 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 fn local_sec(&self) -> i64 {
43 self.sec + self.utc_offset as i64
44 }
45
46 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 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 let days_from_epoch = local.div_euclid(86400);
63
64 let jdn = days_from_epoch + 2440588;
66
67 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 let wday = (jdn + 1).rem_euclid(7) as u32; 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 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
103fn 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
120fn 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
143fn 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 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
161fn 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
180fn 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
192fn 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
200fn 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
208fn 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
216fn 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
224fn 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
232fn 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
240fn 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
247fn 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
254fn 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
277fn 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 if let RValue::Data(_) = &args[0].value
290 && let Ok(rhs) = get_time_data(&args[0])
291 {
292 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
310fn 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
339fn 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
346fn 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 } 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
364fn 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
371fn 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#[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#[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
423fn 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
469fn 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
484pub fn init_time(vm: &mut VM) {
491 let time_class = vm.define_class("Time", None, None);
492
493 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 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 #[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#[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}