use crate::host::HostFunction;
use crate::object::function::JSFunction;
use crate::object::object::JSObject;
use crate::runtime::context::JSContext;
use crate::value::JSValue;
use std::time::{SystemTime, UNIX_EPOCH};
fn current_timestamp_ms() -> f64 {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
(now.as_secs() as f64 * 1000.0) + (now.subsec_nanos() as f64 / 1_000_000.0)
}
pub fn register_builtins(ctx: &mut JSContext) {
ctx.register_builtin("date_now", HostFunction::new("now", 0, date_now));
ctx.register_builtin("date_parse", HostFunction::new("parse", 1, date_parse));
ctx.register_builtin("date_utc", HostFunction::new("UTC", 7, date_utc));
ctx.register_builtin(
"date_constructor",
HostFunction::new("Date", 7, date_constructor),
);
ctx.register_builtin(
"date_getTime",
HostFunction::new("getTime", 0, date_get_time),
);
ctx.register_builtin(
"date_getFullYear",
HostFunction::new("getFullYear", 0, date_get_full_year),
);
ctx.register_builtin(
"date_getMonth",
HostFunction::new("getMonth", 0, date_get_month),
);
ctx.register_builtin(
"date_getDate",
HostFunction::new("getDate", 0, date_get_date),
);
ctx.register_builtin("date_getDay", HostFunction::new("getDay", 0, date_get_day));
ctx.register_builtin(
"date_getHours",
HostFunction::new("getHours", 0, date_get_hours),
);
ctx.register_builtin(
"date_getMinutes",
HostFunction::new("getMinutes", 0, date_get_minutes),
);
ctx.register_builtin(
"date_getSeconds",
HostFunction::new("getSeconds", 0, date_get_seconds),
);
ctx.register_builtin(
"date_getMilliseconds",
HostFunction::new("getMilliseconds", 0, date_get_milliseconds),
);
ctx.register_builtin(
"date_getTimezoneOffset",
HostFunction::new("getTimezoneOffset", 0, date_get_timezone_offset),
);
ctx.register_builtin(
"date_toString",
HostFunction::new("toString", 0, date_to_string),
);
ctx.register_builtin(
"date_toISOString",
HostFunction::new("toISOString", 0, date_to_iso_string),
);
ctx.register_builtin(
"date_toUTCString",
HostFunction::new("toUTCString", 0, date_to_utc_string),
);
ctx.register_builtin(
"date_toDateString",
HostFunction::new("toDateString", 0, date_to_date_string),
);
ctx.register_builtin(
"date_toTimeString",
HostFunction::new("toTimeString", 0, date_to_time_string),
);
ctx.register_builtin(
"date_valueOf",
HostFunction::new("valueOf", 0, date_value_of),
);
}
pub fn init_date(ctx: &mut JSContext) {
let date_atom = ctx.intern("Date");
let mut date_func = JSFunction::new_builtin(date_atom, 7);
date_func.set_builtin_marker(ctx, "date_constructor");
date_func
.base
.set(ctx.intern("now"), create_builtin_function(ctx, "date_now"));
date_func.base.set(
ctx.intern("parse"),
create_builtin_function(ctx, "date_parse"),
);
date_func
.base
.set(ctx.intern("UTC"), create_builtin_function(ctx, "date_utc"));
let date_ptr = Box::into_raw(Box::new(date_func)) as usize;
ctx.runtime_mut().gc_heap_mut().track_function(date_ptr);
let date_value = JSValue::new_function(date_ptr);
let global = ctx.global();
if global.is_object() {
let global_obj = global.as_object_mut();
global_obj.set(date_atom, date_value);
}
let proto_atom = ctx.intern("DatePrototype");
let mut proto_obj = JSObject::new();
if let Some(obj_proto_ptr) = ctx.get_object_prototype() {
proto_obj.prototype = Some(obj_proto_ptr);
}
proto_obj.set(
ctx.intern("getTime"),
create_builtin_function(ctx, "date_getTime"),
);
proto_obj.set(
ctx.intern("getFullYear"),
create_builtin_function(ctx, "date_getFullYear"),
);
proto_obj.set(
ctx.intern("getMonth"),
create_builtin_function(ctx, "date_getMonth"),
);
proto_obj.set(
ctx.intern("getDate"),
create_builtin_function(ctx, "date_getDate"),
);
proto_obj.set(
ctx.intern("getDay"),
create_builtin_function(ctx, "date_getDay"),
);
proto_obj.set(
ctx.intern("getHours"),
create_builtin_function(ctx, "date_getHours"),
);
proto_obj.set(
ctx.intern("getMinutes"),
create_builtin_function(ctx, "date_getMinutes"),
);
proto_obj.set(
ctx.intern("getSeconds"),
create_builtin_function(ctx, "date_getSeconds"),
);
proto_obj.set(
ctx.intern("getMilliseconds"),
create_builtin_function(ctx, "date_getMilliseconds"),
);
proto_obj.set(
ctx.intern("getTimezoneOffset"),
create_builtin_function(ctx, "date_getTimezoneOffset"),
);
proto_obj.set(
ctx.intern("toString"),
create_builtin_function(ctx, "date_toString"),
);
proto_obj.set(
ctx.intern("toISOString"),
create_builtin_function(ctx, "date_toISOString"),
);
proto_obj.set(
ctx.intern("toUTCString"),
create_builtin_function(ctx, "date_toUTCString"),
);
proto_obj.set(
ctx.intern("toDateString"),
create_builtin_function(ctx, "date_toDateString"),
);
proto_obj.set(
ctx.intern("toTimeString"),
create_builtin_function(ctx, "date_toTimeString"),
);
proto_obj.set(
ctx.intern("valueOf"),
create_builtin_function(ctx, "date_valueOf"),
);
let proto_ptr = Box::into_raw(Box::new(proto_obj)) as usize;
ctx.runtime_mut().gc_heap_mut().track(proto_ptr);
let proto_value = JSValue::new_object(proto_ptr);
if global.is_object() {
let global_obj = global.as_object_mut();
global_obj.set(proto_atom, proto_value);
}
let date_func_ref = date_value.as_function_mut();
date_func_ref.base.set(ctx.intern("prototype"), proto_value);
}
fn create_builtin_function(ctx: &mut JSContext, name: &str) -> JSValue {
let mut func = crate::object::function::JSFunction::new_builtin(ctx.intern(name), 0);
func.set_builtin_marker(ctx, name);
let ptr = Box::into_raw(Box::new(func)) as usize;
ctx.runtime_mut().gc_heap_mut().track_function(ptr);
JSValue::new_function(ptr)
}
pub fn date_now(_ctx: &mut JSContext, _args: &[JSValue]) -> JSValue {
JSValue::new_float(current_timestamp_ms())
}
pub fn date_parse(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
if args.is_empty() {
return JSValue::new_float(f64::NAN);
}
let date_str = if args[0].is_string() {
ctx.get_atom_str(args[0].get_atom()).to_string()
} else {
return JSValue::new_float(f64::NAN);
};
if let Ok(ts) = parse_iso_date(&date_str) {
return JSValue::new_int(ts);
}
if let Ok(ts) = parse_common_date(&date_str) {
return JSValue::new_int(ts);
}
JSValue::new_float(f64::NAN)
}
fn parse_iso_date(s: &str) -> Result<i64, ()> {
let s = s.trim();
let parts: Vec<&str> = s.split('T').collect();
let date_part = parts[0];
let date_components: Vec<&str> = date_part.split('-').collect();
if date_components.len() != 3 {
return Err(());
}
let year: i32 = date_components[0].parse().map_err(|_| ())?;
let month: u32 = date_components[1].parse().map_err(|_| ())?;
let day: u32 = date_components[2].parse().map_err(|_| ())?;
let mut hour: u32 = 0;
let mut minute: u32 = 0;
let mut second: u32 = 0;
if parts.len() > 1 {
let time_part = parts[1].trim_end_matches('Z');
let time_components: Vec<&str> = time_part.split(':').collect();
if time_components.len() >= 2 {
hour = time_components[0].parse().map_err(|_| ())?;
minute = time_components[1].parse().map_err(|_| ())?;
if time_components.len() >= 3 {
let sec_str = time_components[2];
let sec_parts: Vec<&str> = sec_str.split('.').collect();
second = sec_parts[0].parse().map_err(|_| ())?;
}
}
}
Ok(date_to_timestamp(year, month, day, hour, minute, second))
}
fn parse_common_date(s: &str) -> Result<i64, ()> {
let s = s.trim();
let parts: Vec<&str> = s.split_whitespace().collect();
if parts.len() >= 3 {
let mut year: i32 = 0;
let mut month: u32 = 0;
let mut day: u32 = 0;
let hour: u32 = 0;
let minute: u32 = 0;
let second: u32 = 0;
for part in &parts {
if let Ok(y) = part.parse::<i32>() {
if y > 1000 {
year = y;
} else if y > 0 && y <= 31 {
day = y as u32;
}
} else if let Ok(d) = part.parse::<u32>() {
if d > 0 && d <= 31 {
day = d;
}
} else {
month = month_from_name(part);
}
}
if year > 0 && month > 0 && day > 0 {
return Ok(date_to_timestamp(year, month, day, hour, minute, second));
}
}
Err(())
}
fn month_from_name(name: &str) -> u32 {
match name.to_lowercase().as_str() {
"jan" | "january" => 1,
"feb" | "february" => 2,
"mar" | "march" => 3,
"apr" | "april" => 4,
"may" => 5,
"jun" | "june" => 6,
"jul" | "july" => 7,
"aug" | "august" => 8,
"sep" | "september" => 9,
"oct" | "october" => 10,
"nov" | "november" => 11,
"dec" | "december" => 12,
_ => 0,
}
}
fn date_to_timestamp(year: i32, month: u32, day: u32, hour: u32, minute: u32, second: u32) -> i64 {
let mut days: i64 = 0;
let month = month.clamp(1, 12);
let day = day.max(1);
let hour = hour.min(23);
let minute = minute.min(59);
let second = second.min(59);
for y in 1970..year {
days += if is_leap_year(y) { 366 } else { 365 };
}
for y in year..1970 {
days -= if is_leap_year(y) { 366 } else { 365 };
}
let days_in_months = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
for m in 1..month {
days += days_in_months[(m - 1) as usize] as i64;
if m == 2 && is_leap_year(year) {
days += 1;
}
}
days += (day - 1) as i64;
let ms = days * 24 * 60 * 60 * 1000
+ hour as i64 * 60 * 60 * 1000
+ minute as i64 * 60 * 1000
+ second as i64 * 1000;
ms
}
fn is_leap_year(year: i32) -> bool {
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
}
pub fn date_utc(_ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
if args.len() < 3 {
return JSValue::new_float(f64::NAN);
}
let year = args[0].get_int() as i32;
let month = args[1].get_int() as u32;
let day = if args.len() > 2 {
args[2].get_int() as u32
} else {
1
};
let hour = if args.len() > 3 {
args[3].get_int() as u32
} else {
0
};
let minute = if args.len() > 4 {
args[4].get_int() as u32
} else {
0
};
let second = if args.len() > 5 {
args[5].get_int() as u32
} else {
0
};
JSValue::new_int(date_to_timestamp(
year,
month + 1,
day,
hour,
minute,
second,
))
}
pub fn date_get_time(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
if args.is_empty() {
return JSValue::new_float(f64::NAN);
}
if args[0].is_object() {
let obj = args[0].as_object();
let key = ctx.intern("__dateValue__");
if let Some(ts) = obj.get(key) {
return ts;
}
let vkey = ctx.common_atoms.__value__;
if let Some(ts) = obj.get(vkey) {
return ts;
}
}
JSValue::new_float(f64::NAN)
}
pub fn date_get_full_year(_ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
let timestamp = get_date_timestamp_with_ctx(
&args.get(0).cloned().unwrap_or_else(JSValue::undefined),
_ctx,
);
if timestamp.is_nan() {
return JSValue::new_float(f64::NAN);
}
let (year, _, _, _, _, _, _) = timestamp_to_date(timestamp);
JSValue::new_int(year as i64)
}
pub fn date_get_month(_ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
let timestamp = get_date_timestamp_with_ctx(
&args.get(0).cloned().unwrap_or_else(JSValue::undefined),
_ctx,
);
if timestamp.is_nan() {
return JSValue::new_float(f64::NAN);
}
let (_, month, _, _, _, _, _) = timestamp_to_date(timestamp);
JSValue::new_int(month as i64)
}
pub fn date_get_date(_ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
let timestamp = get_date_timestamp_with_ctx(
&args.get(0).cloned().unwrap_or_else(JSValue::undefined),
_ctx,
);
if timestamp.is_nan() {
return JSValue::new_float(f64::NAN);
}
let (_, _, day, _, _, _, _) = timestamp_to_date(timestamp);
JSValue::new_int(day as i64)
}
pub fn date_get_day(_ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
let timestamp = get_date_timestamp_with_ctx(
&args.get(0).cloned().unwrap_or_else(JSValue::undefined),
_ctx,
);
if timestamp.is_nan() {
return JSValue::new_float(f64::NAN);
}
let (_, _, _, _, _, _, weekday) = timestamp_to_date(timestamp);
JSValue::new_int(weekday as i64)
}
pub fn date_get_hours(_ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
let timestamp = get_date_timestamp_with_ctx(
&args.get(0).cloned().unwrap_or_else(JSValue::undefined),
_ctx,
);
if timestamp.is_nan() {
return JSValue::new_float(f64::NAN);
}
let (_, _, _, hour, _, _, _) = timestamp_to_date(timestamp);
JSValue::new_int(hour as i64)
}
pub fn date_get_minutes(_ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
let timestamp = get_date_timestamp_with_ctx(
&args.get(0).cloned().unwrap_or_else(JSValue::undefined),
_ctx,
);
if timestamp.is_nan() {
return JSValue::new_float(f64::NAN);
}
let (_, _, _, _, minute, _, _) = timestamp_to_date(timestamp);
JSValue::new_int(minute as i64)
}
pub fn date_get_seconds(_ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
let timestamp = get_date_timestamp_with_ctx(
&args.get(0).cloned().unwrap_or_else(JSValue::undefined),
_ctx,
);
if timestamp.is_nan() {
return JSValue::new_float(f64::NAN);
}
let (_, _, _, _, _, second, _) = timestamp_to_date(timestamp);
JSValue::new_int(second as i64)
}
pub fn date_get_milliseconds(_ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
let timestamp = get_date_timestamp_with_ctx(
&args.get(0).cloned().unwrap_or_else(JSValue::undefined),
_ctx,
);
if timestamp.is_nan() {
return JSValue::new_float(f64::NAN);
}
let ms = (timestamp % 1000.0).abs();
JSValue::new_int(ms as i64)
}
pub fn date_get_timezone_offset(_ctx: &mut JSContext, _args: &[JSValue]) -> JSValue {
JSValue::new_int(0)
}
pub fn date_to_string(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
let timestamp = get_date_timestamp_with_ctx(
&args.get(0).cloned().unwrap_or_else(JSValue::undefined),
ctx,
);
if timestamp.is_nan() {
return JSValue::new_string(ctx.intern("Invalid Date"));
}
let (year, month, day, hour, minute, second, weekday) = timestamp_to_date(timestamp);
let weekday_names = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
let month_names = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
];
let result = format!(
"{} {} {:02} {:04} {:02}:{:02}:{:02} GMT+0000 (UTC)",
weekday_names[weekday as usize],
month_names[(month - 1) as usize],
day,
year,
hour,
minute,
second
);
JSValue::new_string(ctx.intern(&result))
}
pub fn date_to_iso_string(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
let timestamp = get_date_timestamp_with_ctx(
&args.get(0).cloned().unwrap_or_else(JSValue::undefined),
ctx,
);
if timestamp.is_nan() {
return JSValue::new_string(ctx.intern("Invalid Date"));
}
let (year, month, day, hour, minute, second, _) = timestamp_to_date(timestamp);
let result = format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.000Z",
year, month, day, hour, minute, second
);
JSValue::new_string(ctx.intern(&result))
}
pub fn date_to_utc_string(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
date_to_string(ctx, args)
}
pub fn date_to_date_string(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
let timestamp = get_date_timestamp_with_ctx(
&args.get(0).cloned().unwrap_or_else(JSValue::undefined),
ctx,
);
if timestamp.is_nan() {
return JSValue::new_string(ctx.intern("Invalid Date"));
}
let (year, month, day, _, _, _, weekday) = timestamp_to_date(timestamp);
let weekday_names = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
let month_names = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
];
let result = format!(
"{} {} {:02} {:04}",
weekday_names[weekday as usize],
month_names[(month - 1) as usize],
day,
year
);
JSValue::new_string(ctx.intern(&result))
}
pub fn date_to_time_string(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
let timestamp = get_date_timestamp_with_ctx(
&args.get(0).cloned().unwrap_or_else(JSValue::undefined),
ctx,
);
if timestamp.is_nan() {
return JSValue::new_string(ctx.intern("Invalid Date"));
}
let (_, _, _, hour, minute, second, _) = timestamp_to_date(timestamp);
let result = format!("{:02}:{:02}:{:02} GMT+0000 (UTC)", hour, minute, second);
JSValue::new_string(ctx.intern(&result))
}
pub fn date_value_of(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
if args.is_empty() || !args[0].is_object() {
return JSValue::new_float(f64::NAN);
}
let obj = args[0].as_object();
let key = ctx.intern("__dateValue__");
if let Some(ts) = obj.get(key) {
return ts;
}
let vkey = ctx.common_atoms.__value__;
if let Some(ts) = obj.get(vkey) {
return ts;
}
JSValue::new_float(f64::NAN)
}
pub fn init_date_to_primitive(ctx: &mut JSContext) {
let global = ctx.global();
if !global.is_object() {
return;
}
let global_obj = global.as_object();
let stp_key = ctx.intern("Symbol.toPrimitive");
let sym_val = match global_obj.get(stp_key) {
Some(s) if s.is_symbol() => s,
_ => return,
};
let sym_atom = crate::runtime::atom::Atom(0x40000000 | sym_val.get_symbol_id());
let date_key = ctx.intern("Date");
let date_ctor = match global_obj.get(date_key) {
Some(v) if v.is_function() => v,
_ => return,
};
let proto_key = ctx.intern("prototype");
let proto_val = date_ctor.as_object().get(proto_key);
if let Some(pv) = proto_val {
if pv.is_object() {
pv.as_object_mut()
.set(sym_atom, create_builtin_function(ctx, "date_toPrimitive"));
}
}
}
pub fn date_to_primitive(ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
if args.len() < 2 {
return JSValue::undefined();
}
let hint_val = &args[1];
let hint = if hint_val.is_string() {
ctx.get_atom_str(hint_val.get_atom())
} else {
"default"
};
match hint {
"string" | "default" => date_to_string(ctx, args),
"number" => date_get_time(ctx, args),
_ => date_to_string(ctx, args),
}
}
fn get_date_timestamp_with_ctx(this: &JSValue, ctx: &mut JSContext) -> f64 {
if this.is_object() {
let obj = this.as_object();
if let Some(ts) = obj.get(ctx.intern("__dateValue__")) {
if ts.is_int() {
return ts.get_int() as f64;
} else if ts.is_float() {
return ts.get_float();
}
}
if let Some(ts) = obj.get(ctx.common_atoms.__value__) {
if ts.is_int() {
return ts.get_int() as f64;
} else if ts.is_float() {
return ts.get_float();
}
}
}
f64::NAN
}
fn timestamp_to_date(ms: f64) -> (i32, u32, u32, u32, u32, u32, u32) {
if ms.is_nan() || ms.is_infinite() {
return (0, 0, 0, 0, 0, 0, 0);
}
let total_ms = ms as i64;
let total_seconds = total_ms / 1000;
let total_minutes = total_seconds / 60;
let total_hours = total_minutes / 60;
let total_days = total_hours / 24;
let mut year = 1970;
let mut remaining_days = total_days;
if remaining_days >= 0 {
while remaining_days >= days_in_year(year) {
remaining_days -= days_in_year(year);
year += 1;
}
} else {
while remaining_days < 0 {
year -= 1;
remaining_days += days_in_year(year);
}
}
let days_in_months = if is_leap_year(year) {
[31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
} else {
[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
};
let mut month = 1;
for &days in &days_in_months {
if remaining_days < days as i64 {
break;
}
remaining_days -= days as i64;
month += 1;
}
let day = (remaining_days + 1) as u32;
let hour = ((total_hours % 24 + 24) % 24) as u32;
let minute = ((total_minutes % 60 + 60) % 60) as u32;
let second = ((total_seconds % 60 + 60) % 60) as u32;
let weekday = ((total_days % 7 + 4 + 7) % 7) as u32;
(year, month, day, hour, minute, second, weekday)
}
fn days_in_year(year: i32) -> i64 {
if is_leap_year(year) { 366 } else { 365 }
}
pub fn date_constructor(_ctx: &mut JSContext, args: &[JSValue]) -> JSValue {
let timestamp = if args.is_empty() {
current_timestamp_ms()
} else if args.len() == 1 {
if args[0].is_int() {
args[0].get_int() as f64
} else if args[0].is_float() {
args[0].get_float()
} else if args[0].is_string() {
current_timestamp_ms()
} else {
return JSValue::new_float(f64::NAN);
}
} else {
let year = args[0].get_int() as i32;
let month = if args.len() > 1 {
args[1].get_int() as u32
} else {
0
};
let day = if args.len() > 2 {
args[2].get_int() as u32
} else {
1
};
let hour = if args.len() > 3 {
args[3].get_int() as u32
} else {
0
};
let minute = if args.len() > 4 {
args[4].get_int() as u32
} else {
0
};
let second = if args.len() > 5 {
args[5].get_int() as u32
} else {
0
};
date_to_timestamp(year, month + 1, day.max(1), hour, minute, second) as f64
};
if timestamp.is_nan() {
return JSValue::new_float(f64::NAN);
}
JSValue::new_float(timestamp)
}