use super::eval;
use crate::util::float;
use crate::value::Value;
use alloc::string::String;
use alloc::vec::Vec;
const JD_MAX: i64 = 464_269_060_799_999;
#[derive(Clone, Copy, Default)]
struct DateTime {
ijd: i64, y: i32, m: i32, d: i32, h: i32, min: i32, s: f64, tz: i32, valid_jd: bool,
valid_ymd: bool,
valid_hms: bool,
valid_tz: bool,
raw_s: bool, subsec: bool, n_floor: i32, }
impl DateTime {
fn clear_ymd_hms_tz(&mut self) {
self.valid_ymd = false;
self.valid_hms = false;
self.valid_tz = false;
}
fn compute_jd(&mut self) {
if self.valid_jd {
return;
}
let (mut year, mut month, day) = if self.valid_ymd {
(self.y, self.m, self.d)
} else {
(2000, 1, 1)
};
if !(-4713..=9999).contains(&year) || self.raw_s {
self.ijd = i64::MIN;
self.valid_jd = true;
return;
}
if month <= 2 {
year -= 1;
month += 12;
}
let year = year as i64;
let month = month as i64;
let a = year / 100;
let b = 2 - a + a / 4;
let x1 = 36525 * (year + 4716) / 100;
let x2 = 306001 * (month + 1) / 10000;
self.ijd = (((x1 + x2 + day as i64 + b) as f64 - 1524.5) * 86_400_000.0) as i64;
self.valid_jd = true;
if self.valid_hms {
self.ijd += self.h as i64 * 3_600_000
+ self.min as i64 * 60_000
+ (self.s * 1000.0 + 0.5) as i64;
if self.valid_tz {
self.ijd -= self.tz as i64 * 60_000;
self.valid_ymd = false;
self.valid_hms = false;
self.valid_tz = false;
}
}
}
fn compute_floor(&mut self) {
let m = self.m;
let d = self.d;
self.n_floor = if d <= 28 {
0
} else if (1 << m) & 0x15aa != 0 {
0
} else if m != 2 {
i32::from(d == 31)
} else if self.y % 4 != 0 || (self.y % 100 == 0 && self.y % 400 != 0) {
d - 28
} else {
d - 29
};
}
fn compute_ymd(&mut self) {
if self.valid_ymd {
return;
}
if !self.valid_jd {
self.y = 2000;
self.m = 1;
self.d = 1;
} else {
let z = ((self.ijd + 43_200_000) / 86_400_000) as i32;
let mut a = ((z as f64 - 1_867_216.25) / 36_524.25) as i32;
a = z + 1 + a - a / 4;
let b = a + 1524;
let c = ((b as f64 - 122.1) / 365.25) as i32;
let d = 36525 * (c & 32767) / 100;
let e = ((b - d) as f64 / 30.6001) as i32;
let x1 = (30.6001 * e as f64) as i32;
self.d = b - d - x1;
self.m = if e < 14 { e - 1 } else { e - 13 };
self.y = if self.m > 2 { c - 4716 } else { c - 4715 };
}
self.valid_ymd = true;
}
fn compute_hms(&mut self) {
if self.valid_hms {
return;
}
self.compute_jd();
let mut s = ((self.ijd + 43_200_000) % 86_400_000) as i32;
self.s = s as f64 / 1000.0;
s = self.s as i32;
self.s -= s as f64;
self.h = s / 3600;
s -= self.h * 3600;
self.min = s / 60;
self.s += (s - self.min * 60) as f64;
self.valid_hms = true;
}
fn compute_ymd_hms(&mut self) {
self.compute_ymd();
self.compute_hms();
}
}
fn set_raw_date_number(p: &mut DateTime, r: f64) {
p.s = r;
p.raw_s = true;
if (0.0..5_373_484.5).contains(&r) {
p.ijd = (r * 86_400_000.0 + 0.5) as i64;
p.valid_jd = true;
}
}
fn parse_yyyy_mm_dd(z: &str, p: &mut DateTime) -> bool {
let bytes = z.as_bytes();
let mut i = 0;
let neg = if bytes.first() == Some(&b'-') {
i = 1;
true
} else {
false
};
let (year, ni) = read_exact(bytes, i, 4);
let Some(year) = year else { return false };
i = ni;
if bytes.get(i) != Some(&b'-') {
return false;
}
i += 1;
let (month, ni) = read_exact(bytes, i, 2);
let Some(month) = month else { return false };
i = ni;
if bytes.get(i) != Some(&b'-') {
return false;
}
i += 1;
let (day, ni) = read_exact(bytes, i, 2);
let Some(day) = day else { return false };
i = ni;
if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
return false;
}
while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'T' || bytes[i] == b't') {
i += 1;
}
if i < bytes.len() {
if !parse_hh_mm_ss(&z[i..], p) {
return false;
}
} else {
p.valid_hms = false;
p.valid_tz = false;
}
p.valid_jd = false;
p.valid_ymd = true;
p.y = if neg { -year } else { year };
p.m = month;
p.d = day;
p.compute_floor();
true
}
fn parse_hh_mm_ss(z: &str, p: &mut DateTime) -> bool {
let bytes = z.as_bytes();
let mut i = 0;
let (h, ni) = read_exact(bytes, i, 2);
let Some(h) = h else { return false };
i = ni;
if bytes.get(i) != Some(&b':') {
return false;
}
i += 1;
let (min, ni) = read_exact(bytes, i, 2);
let Some(min) = min else { return false };
i = ni;
let mut sec = 0.0;
if bytes.get(i) == Some(&b':') {
i += 1;
let (s, ni) = read_exact(bytes, i, 2);
let Some(s) = s else { return false };
i = ni;
sec = s as f64;
if bytes.get(i) == Some(&b'.') {
i += 1;
let start = i;
let mut scale = 1.0;
let mut frac = 0.0;
while i < bytes.len() && bytes[i].is_ascii_digit() {
scale *= 10.0;
frac = frac * 10.0 + (bytes[i] - b'0') as f64;
i += 1;
}
if i == start {
return false;
}
sec += frac / scale;
}
}
if !(0..=24).contains(&h) || !(0..=59).contains(&min) || !(0.0..60.0).contains(&sec) {
return false;
}
p.valid_jd = false;
p.valid_hms = true;
p.h = h;
p.min = min;
p.s = sec;
parse_timezone(z, i, p)
}
fn parse_timezone(z: &str, mut i: usize, p: &mut DateTime) -> bool {
let bytes = z.as_bytes();
while i < bytes.len() && bytes[i] == b' ' {
i += 1;
}
p.tz = 0;
p.valid_tz = false;
if i >= bytes.len() {
return true;
}
let sign = match bytes[i] {
b'Z' | b'z' => {
i += 1;
p.valid_tz = true;
return i == bytes.len();
}
b'+' => 1,
b'-' => -1,
_ => return false,
};
i += 1;
let (th, ni) = read_exact(bytes, i, 2);
let Some(th) = th.filter(|h| (0..=14).contains(h)) else {
return false;
};
i = ni;
if bytes.get(i) != Some(&b':') {
return false;
}
i += 1;
let (tm, ni) = read_exact(bytes, i, 2);
let Some(tm) = tm.filter(|m| (0..=59).contains(m)) else {
return false;
};
i = ni;
p.tz = sign * (th * 60 + tm);
p.valid_tz = true;
i == bytes.len()
}
fn read_exact(bytes: &[u8], start: usize, len: usize) -> (Option<i32>, usize) {
let mut val: i32 = 0;
for k in 0..len {
match bytes.get(start + k) {
Some(b) if b.is_ascii_digit() => val = val * 10 + (b - b'0') as i32,
_ => return (None, start),
}
}
(Some(val), start + len)
}
fn parse_value(v: &Value) -> Option<DateTime> {
let mut p = DateTime::default();
match v {
Value::Null => None,
Value::Integer(i) => {
set_raw_date_number(&mut p, *i as f64);
Some(p)
}
Value::Real(r) => {
set_raw_date_number(&mut p, *r);
Some(p)
}
Value::Text(s) => {
let z = s.as_str();
if parse_yyyy_mm_dd(z, &mut p) || parse_hh_mm_ss(z, &mut p) {
Some(p)
} else if z.eq_ignore_ascii_case("now") {
set_to_now(&mut p).then_some(p)
} else if let Some(r) = parse_float(z) {
set_raw_date_number(&mut p, r);
Some(p)
} else {
None
}
}
Value::Blob(_) => None,
}
}
fn set_to_now(p: &mut DateTime) -> bool {
match current_ijd() {
Some(ijd) => {
p.clear_ymd_hms_tz();
p.ijd = ijd;
p.valid_jd = true;
p.raw_s = false;
true
}
None => false,
}
}
#[cfg(feature = "std")]
fn current_ijd() -> Option<i64> {
use std::time::{SystemTime, UNIX_EPOCH};
let ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.ok()?
.as_millis() as i64;
Some(ms + 210_866_760_000_000)
}
#[cfg(not(feature = "std"))]
fn current_ijd() -> Option<i64> {
None
}
fn parse_float(s: &str) -> Option<f64> {
let t = s.trim();
if t.is_empty() {
return None;
}
t.parse::<f64>().ok()
}
fn apply_modifier(p: &mut DateTime, m: &str, idx: usize) -> bool {
let lower = m.to_ascii_lowercase();
match lower.as_str() {
"utc" | "localtime" => true,
"unixepoch" => {
if idx > 1 || !p.raw_s {
return false;
}
let r = p.s * 1000.0 + 210_866_760_000_000.0;
if (0.0..464_269_060_800_000.0).contains(&r) {
p.clear_ymd_hms_tz();
p.ijd = (r + 0.5) as i64;
p.valid_jd = true;
p.raw_s = false;
return true;
}
false
}
"julianday" => {
if idx > 1 {
return false;
}
if p.raw_s {
p.raw_s = false;
}
true
}
"auto" => {
if idx > 1 {
return false;
}
if p.raw_s {
if p.s >= 0.0 && p.s < 5_373_484.5 {
p.raw_s = false;
} else {
return apply_modifier(p, "unixepoch", idx);
}
}
true
}
"start of day" => {
p.compute_ymd_hms();
p.s = 0.0;
p.min = 0;
p.h = 0;
p.valid_jd = false;
p.valid_hms = true;
p.valid_tz = false;
p.compute_jd();
true
}
"start of month" => {
p.compute_ymd();
p.d = 1;
p.s = 0.0;
p.min = 0;
p.h = 0;
p.valid_jd = false;
p.valid_hms = true;
p.valid_tz = false;
p.compute_jd();
true
}
"start of year" => {
p.compute_ymd();
p.m = 1;
p.d = 1;
p.s = 0.0;
p.min = 0;
p.h = 0;
p.valid_jd = false;
p.valid_hms = true;
p.valid_tz = false;
p.compute_jd();
true
}
"ceiling" => {
p.compute_jd();
p.clear_ymd_hms_tz();
p.n_floor = 0;
true
}
"floor" => {
p.compute_jd();
p.ijd -= p.n_floor as i64 * 86_400_000;
p.clear_ymd_hms_tz();
true
}
"subsec" | "subsecond" => {
p.subsec = true;
true
}
_ => apply_numeric_modifier(p, m, &lower),
}
}
fn apply_numeric_modifier(p: &mut DateTime, orig: &str, lower: &str) -> bool {
if let Some(rest) = lower.strip_prefix("weekday ") {
let Some(r) = parse_float(rest) else {
return false;
};
if !(0.0..7.0).contains(&r) || r != float::trunc(r) {
return false;
}
let n = r as i64;
p.compute_ymd_hms();
p.valid_jd = false;
p.compute_jd();
let mut z = (p.ijd + 129_600_000) / 86_400_000 % 7; if z > n {
z -= 7;
}
p.ijd += (n - z) * 86_400_000;
p.clear_ymd_hms_tz();
return true;
}
if (orig.starts_with('+') || orig.starts_with('-')) && apply_ymd_offset(p, orig) {
return true;
}
let bytes = orig.as_bytes();
let mut i = 0;
if i < bytes.len() && (bytes[i] == b'+' || bytes[i] == b'-') {
i += 1;
}
while i < bytes.len() && (bytes[i].is_ascii_digit() || bytes[i] == b'.') {
i += 1;
}
if i == 0 || (i == 1 && !bytes[0].is_ascii_digit()) {
return false;
}
if bytes.get(i) == Some(&b':') {
return apply_time_shift(p, orig);
}
let Some(r) = parse_float(&orig[..i]) else {
return false;
};
let unit_field = &orig[i..];
let unit_trimmed = unit_field.trim_start();
if unit_trimmed.len() == unit_field.len() && !unit_field.is_empty() {
return false;
}
let unit = unit_trimmed.to_ascii_lowercase();
let rounder = if r < 0.0 { -0.5 } else { 0.5 };
let limit = match unit.as_str() {
"second" | "seconds" => 4.6427e14_f64,
"minute" | "minutes" => 7.7379e12,
"hour" | "hours" => 1.2897e11,
"day" | "days" => 5_373_485.0,
"month" | "months" => 176_546.0,
"year" | "years" => 14713.0,
_ => return false,
};
if !(-limit < r && r < limit) {
return false;
}
p.n_floor = 0;
match unit.as_str() {
"day" | "days" => {
p.compute_jd();
p.ijd += (r * 86_400_000.0 + rounder) as i64;
p.clear_ymd_hms_tz();
}
"hour" | "hours" => {
p.compute_jd();
p.ijd += (r * 3_600_000.0 + rounder) as i64;
p.clear_ymd_hms_tz();
}
"minute" | "minutes" => {
p.compute_jd();
p.ijd += (r * 60_000.0 + rounder) as i64;
p.clear_ymd_hms_tz();
}
"second" | "seconds" => {
p.compute_jd();
p.ijd += (r * 1000.0 + rounder) as i64;
p.clear_ymd_hms_tz();
}
"month" | "months" => {
p.compute_ymd_hms();
p.m += r as i32;
let x = if p.m > 0 {
(p.m - 1) / 12
} else {
(p.m - 12) / 12
};
p.y += x;
p.m -= x * 12;
p.compute_floor();
p.valid_jd = false;
p.compute_jd();
let frac = r - float::trunc(r);
if frac != 0.0 {
p.ijd += (frac * 30.0 * 86_400_000.0 + rounder) as i64;
}
p.clear_ymd_hms_tz();
}
"year" | "years" => {
p.compute_ymd_hms();
p.y += r as i32;
p.compute_floor();
p.valid_jd = false;
p.compute_jd();
let frac = r - float::trunc(r);
if frac != 0.0 {
p.ijd += (frac * 365.0 * 86_400_000.0 + rounder) as i64;
}
p.clear_ymd_hms_tz();
}
_ => return false,
}
true
}
fn apply_ymd_offset(p: &mut DateTime, orig: &str) -> bool {
let bytes = orig.as_bytes();
let sign = bytes[0];
let mut n = 1;
let mut dash_at = None;
while n < bytes.len() {
let c = bytes[n];
if c == b':' || c == b' ' {
break;
}
if c == b'-' {
if (n == 5 || n == 6) && dash_at.is_none() {
dash_at = Some(n);
}
}
n += 1;
}
let Some(dash) = dash_at else { return false };
let year_w = dash - 1;
if year_w != 4 && year_w != 5 {
return false;
}
let head = &orig[1..n];
let parts: Vec<&str> = head.split('-').collect();
if parts.len() != 3 || parts[1].len() != 2 || parts[2].len() != 2 {
return false;
}
let (Ok(yy), Ok(mm), Ok(dd)) = (
parts[0].parse::<i32>(),
parts[1].parse::<i32>(),
parts[2].parse::<i32>(),
) else {
return false;
};
if mm >= 12 || dd >= 31 {
return false;
}
p.compute_ymd_hms();
p.valid_jd = false;
let d_shift = if sign == b'-' {
p.y -= yy;
p.m -= mm;
-dd
} else {
p.y += yy;
p.m += mm;
dd
};
let x = if p.m > 0 {
(p.m - 1) / 12
} else {
(p.m - 12) / 12
};
p.y += x;
p.m -= x * 12;
p.compute_floor();
p.compute_jd();
p.valid_hms = false;
p.valid_ymd = false;
p.ijd += d_shift as i64 * 86_400_000;
if n >= bytes.len() {
return true;
}
if bytes[n] != b' ' {
return false;
}
let mut tx = DateTime::default();
if !parse_hh_mm_ss(&orig[n + 1..], &mut tx) {
return false;
}
let ms = tx.h as i64 * 3_600_000 + tx.min as i64 * 60_000 + (tx.s * 1000.0 + 0.5) as i64;
p.compute_jd();
if sign == b'-' {
p.ijd -= ms;
} else {
p.ijd += ms;
}
p.clear_ymd_hms_tz();
true
}
fn apply_time_shift(p: &mut DateTime, orig: &str) -> bool {
let bytes = orig.as_bytes();
let sign = match bytes.first() {
Some(b'+') => 1.0,
Some(b'-') => -1.0,
_ => return false,
};
let mut tmp = DateTime::default();
if !parse_hh_mm_ss(&orig[1..], &mut tmp) {
return false;
}
let ms = tmp.h as i64 * 3_600_000 + tmp.min as i64 * 60_000 + (tmp.s * 1000.0 + 0.5) as i64;
p.compute_jd();
p.ijd += (sign as i64) * ms;
p.clear_ymd_hms_tz();
true
}
fn is_date(args: &[Value]) -> Option<DateTime> {
let mut p = match args.first() {
Some(v) => parse_value(v)?,
None => {
let mut p = DateTime::default();
if !set_to_now(&mut p) {
return None;
}
p
}
};
let rest = if args.is_empty() { &[][..] } else { &args[1..] };
for (idx, m) in rest.iter().enumerate() {
let Value::Text(ms) = m else { return None };
if !apply_modifier(&mut p, ms, idx + 1) {
return None;
}
}
p.compute_jd();
if !(0..=JD_MAX).contains(&p.ijd) {
return None;
}
if args.len() == 1 && p.valid_ymd && p.d > 28 {
p.valid_ymd = false;
}
p.compute_ymd();
Some(p)
}
fn fmt_date(p: &mut DateTime) -> String {
p.compute_ymd();
if p.y < 0 || p.y > 9999 {
alloc::format!("{:+05}-{:02}-{:02}", p.y, p.m, p.d)
} else {
alloc::format!("{:04}-{:02}-{:02}", p.y, p.m, p.d)
}
}
fn fmt_time(p: &mut DateTime) -> String {
p.compute_hms();
if p.subsec {
alloc::format!("{:02}:{:02}:{:06.3}", p.h, p.min, p.s)
} else {
alloc::format!("{:02}:{:02}:{:02}", p.h, p.min, p.s as i32)
}
}
pub fn date(args: &[Value]) -> Value {
match is_date(args) {
Some(mut p) => Value::Text(fmt_date(&mut p)),
None => Value::Null,
}
}
pub fn time(args: &[Value]) -> Value {
match is_date(args) {
Some(mut p) => Value::Text(fmt_time(&mut p)),
None => Value::Null,
}
}
pub fn datetime(args: &[Value]) -> Value {
match is_date(args) {
Some(mut p) => Value::Text(alloc::format!("{} {}", fmt_date(&mut p), fmt_time(&mut p))),
None => Value::Null,
}
}
pub fn julianday(args: &[Value]) -> Value {
match is_date(args) {
Some(p) => Value::Real(p.ijd as f64 / 86_400_000.0),
None => Value::Null,
}
}
pub fn unixepoch(args: &[Value]) -> Value {
match is_date(args) {
Some(p) => {
let ms = p.ijd - 210_866_760_000_000;
if p.subsec {
Value::Real(ms as f64 / 1000.0)
} else {
Value::Integer(ms / 1000)
}
}
None => Value::Null,
}
}
pub fn timediff(a: &Value, b: &Value) -> Value {
let Some(mut d1) = parse_value(a) else {
return Value::Null;
};
let Some(mut d2) = parse_value(b) else {
return Value::Null;
};
d1.compute_jd();
d2.compute_jd();
d1.compute_ymd_hms();
d2.compute_ymd_hms();
const BIAS: i64 = 148_699_540_800_000;
let (sign, mut y, mut m);
if d1.ijd >= d2.ijd {
sign = '+';
y = d1.y - d2.y;
if y != 0 {
d2.y = d1.y;
d2.valid_jd = false;
d2.compute_jd();
}
m = d1.m - d2.m;
if m < 0 {
y -= 1;
m += 12;
}
if m != 0 {
d2.m = d1.m;
d2.valid_jd = false;
d2.compute_jd();
}
while d1.ijd < d2.ijd {
m -= 1;
if m < 0 {
m = 11;
y -= 1;
}
d2.m -= 1;
if d2.m < 1 {
d2.m = 12;
d2.y -= 1;
}
d2.valid_jd = false;
d2.compute_jd();
}
d1.ijd -= d2.ijd;
d1.ijd += BIAS;
} else {
sign = '-';
y = d2.y - d1.y;
if y != 0 {
d2.y = d1.y;
d2.valid_jd = false;
d2.compute_jd();
}
m = d2.m - d1.m;
if m < 0 {
y -= 1;
m += 12;
}
if m != 0 {
d2.m = d1.m;
d2.valid_jd = false;
d2.compute_jd();
}
while d1.ijd > d2.ijd {
m -= 1;
if m < 0 {
m = 11;
y -= 1;
}
d2.m += 1;
if d2.m > 12 {
d2.m = 1;
d2.y += 1;
}
d2.valid_jd = false;
d2.compute_jd();
}
d1.ijd = d2.ijd - d1.ijd;
d1.ijd += BIAS;
}
d1.valid_ymd = false;
d1.valid_hms = false;
d1.valid_tz = false;
d1.compute_ymd_hms();
Value::Text(alloc::format!(
"{}{:04}-{:02}-{:02} {:02}:{:02}:{:06.3}",
sign,
y,
m,
d1.d - 1,
d1.h,
d1.min,
d1.s
))
}
pub fn strftime(args: &[Value]) -> Value {
if args.len() < 2 {
return Value::Null;
}
let Value::Text(fmt) = &args[0] else {
return Value::Null;
};
let Some(mut p) = is_date(&args[1..]) else {
return Value::Null;
};
match render_strftime(fmt, &mut p) {
Some(s) => Value::Text(s),
None => Value::Null,
}
}
fn render_strftime(fmt: &str, p: &mut DateTime) -> Option<String> {
p.compute_ymd_hms();
let mut out = String::new();
let mut chars = fmt.chars().peekable();
while let Some(c) = chars.next() {
if c != '%' {
out.push(c);
continue;
}
match chars.next() {
Some('d') => out.push_str(&alloc::format!("{:02}", p.d)),
Some('e') => out.push_str(&alloc::format!("{:2}", p.d)),
Some('f') => {
let sec = p.s;
out.push_str(&alloc::format!("{:06.3}", sec));
}
Some('F') => out.push_str(&fmt_date(p)),
Some('H') => out.push_str(&alloc::format!("{:02}", p.h)),
Some('I') => {
let h12 = ((p.h + 11) % 12) + 1;
out.push_str(&alloc::format!("{:02}", h12));
}
Some('j') => out.push_str(&alloc::format!("{:03}", day_of_year(p))),
Some('J') => out.push_str(&eval::format_real(p.ijd as f64 / 86_400_000.0)),
Some('k') => out.push_str(&alloc::format!("{:2}", p.h)),
Some('l') => {
let h12 = ((p.h + 11) % 12) + 1;
out.push_str(&alloc::format!("{:2}", h12));
}
Some('m') => out.push_str(&alloc::format!("{:02}", p.m)),
Some('M') => out.push_str(&alloc::format!("{:02}", p.min)),
Some('p') => out.push_str(if p.h >= 12 { "PM" } else { "AM" }),
Some('P') => out.push_str(if p.h >= 12 { "pm" } else { "am" }),
Some('R') => out.push_str(&alloc::format!("{:02}:{:02}", p.h, p.min)),
Some('s') => {
let secs = (p.ijd - 210_866_760_000_000) / 1000;
out.push_str(&alloc::format!("{}", secs));
}
Some('S') => out.push_str(&alloc::format!("{:02}", p.s as i32)),
Some('T') => out.push_str(&alloc::format!("{:02}:{:02}:{:02}", p.h, p.min, p.s as i32)),
Some('u') => {
let mut wd = ((p.ijd + 129_600_000) / 86_400_000 % 7) as i32; if wd == 0 {
wd = 7;
}
out.push_str(&alloc::format!("{}", wd));
}
Some('w') => {
let wd = (p.ijd + 129_600_000) / 86_400_000 % 7; out.push_str(&alloc::format!("{}", wd));
}
Some('U') => {
let yday0 = day_of_year(p) - 1;
let wday = (p.ijd + 129_600_000) / 86_400_000 % 7; out.push_str(&alloc::format!("{:02}", (yday0 + 7 - wday) / 7));
}
Some('W') => {
let mut y0 = *p;
y0.valid_jd = false;
y0.m = 1;
y0.d = 1;
y0.h = 0;
y0.min = 0;
y0.s = 0.0;
y0.valid_ymd = true;
y0.valid_hms = true;
y0.compute_jd();
let nday = (p.ijd - y0.ijd + 43_200_000) / 86_400_000;
let wd = (p.ijd + 129_600_000) / 86_400_000 % 7;
let wn = (nday + 7 - (if wd != 0 { wd - 1 } else { 6 })) / 7;
out.push_str(&alloc::format!("{:02}", wn));
}
Some('Y') => out.push_str(&alloc::format!("{:04}", p.y)),
Some('G') => {
let (iy, _) = iso_week_date(p);
out.push_str(&alloc::format!("{iy:04}"));
}
Some('g') => {
let (iy, _) = iso_week_date(p);
out.push_str(&alloc::format!("{:02}", iy.rem_euclid(100)));
}
Some('V') => {
let (_, iw) = iso_week_date(p);
out.push_str(&alloc::format!("{iw:02}"));
}
Some('%') => out.push('%'),
Some(_) | None => return None,
}
}
Some(out)
}
fn day_of_year(p: &DateTime) -> i64 {
let midnight = |m: i32, d: i32| {
let mut x = *p;
x.valid_jd = false;
x.m = m;
x.d = d;
x.h = 0;
x.min = 0;
x.s = 0.0;
x.valid_ymd = true;
x.valid_hms = true;
x.compute_jd();
x.ijd
};
((midnight(p.m, p.d) - midnight(1, 1) + 43_200_000) / 86_400_000) + 1
}
fn iso_weekday(p: &DateTime) -> i32 {
let wd = ((p.ijd + 129_600_000) / 86_400_000 % 7) as i32; if wd == 0 {
7
} else {
wd
}
}
fn iso_week_date(p: &DateTime) -> (i32, i32) {
let long_year = |y: i64| {
let f =
|y: i64| (y + y.div_euclid(4) - y.div_euclid(100) + y.div_euclid(400)).rem_euclid(7);
f(y) == 4 || f(y - 1) == 3
};
let doy = day_of_year(p);
let dow = iso_weekday(p) as i64;
let week = (doy - dow + 10) / 7;
let y = p.y as i64;
let (iy, iw) = if week < 1 {
(y - 1, if long_year(y - 1) { 53 } else { 52 })
} else if week > if long_year(y) { 53 } else { 52 } {
(y + 1, 1)
} else {
(y, week)
};
(iy as i32, iw as i32)
}
const PRINTF_LIMIT: usize = 1_000_000_000;
fn group_thousands(s: &str) -> String {
let (sign, rest) = match s.strip_prefix(['+', '-', ' ']) {
Some(r) => (&s[..1], r),
None => ("", s),
};
let dot = rest.find('.').unwrap_or(rest.len());
let (int_part, frac) = rest.split_at(dot);
let n = int_part.len();
let mut grouped = String::with_capacity(n + n / 3);
for (idx, ch) in int_part.chars().enumerate() {
if idx > 0 && (n - idx) % 3 == 0 {
grouped.push(',');
}
grouped.push(ch);
}
alloc::format!("{sign}{grouped}{frac}")
}
fn strip_zeros_keep_one(s: &str) -> String {
let (mant, exp) = match s.find(['e', 'E']) {
Some(pos) => s.split_at(pos),
None => (s, ""),
};
let mant = if mant.contains('.') {
let t = mant.trim_end_matches('0');
if t.ends_with('.') {
alloc::format!("{t}0")
} else {
String::from(t)
}
} else {
alloc::format!("{mant}.0")
};
alloc::format!("{mant}{exp}")
}
pub fn printf(args: &[Value]) -> Value {
if args.is_empty() {
return Value::Null;
}
let Value::Text(fmt) = &args[0] else {
return Value::Null;
};
let mut out = String::new();
let mut arg_idx = 1usize;
let bytes: Vec<char> = fmt.chars().collect();
let mut i = 0;
let mut too_big = false;
while i < bytes.len() {
let c = bytes[i];
if c != '%' {
out.push(c);
i += 1;
continue;
}
i += 1;
if i >= bytes.len() {
break;
}
if bytes[i] == '%' {
out.push('%');
i += 1;
continue;
}
let mut left = false;
let mut zero = false;
let mut plus = false;
let mut space = false;
let mut alt = false;
let mut comma = false;
let mut bang = false;
loop {
match bytes.get(i) {
Some('-') => left = true,
Some('0') => zero = true,
Some('+') => plus = true,
Some(' ') => space = true,
Some('#') => alt = true,
Some(',') => comma = true, Some('!') => bang = true, _ => break,
}
i += 1;
}
let mut width = 0usize;
if bytes.get(i) == Some(&'*') {
i += 1;
let wv = eval::to_i64(&args.get(arg_idx).cloned().unwrap_or(Value::Null));
arg_idx += 1;
if wv < 0 {
left = true;
width = wv.unsigned_abs() as usize;
} else {
width = wv as usize;
}
} else {
while let Some(d) = bytes.get(i).filter(|c| c.is_ascii_digit()) {
width = width
.saturating_mul(10)
.saturating_add((*d as u8 - b'0') as usize);
i += 1;
}
}
if bytes.get(i) == Some(&',') {
too_big = true; break;
}
let mut prec: Option<usize> = None;
if bytes.get(i) == Some(&'.') {
i += 1;
if bytes.get(i) == Some(&'*') {
i += 1;
let pv = eval::to_i64(&args.get(arg_idx).cloned().unwrap_or(Value::Null));
arg_idx += 1;
prec = Some(pv.max(0) as usize);
} else {
let mut p = 0usize;
while let Some(d) = bytes.get(i).filter(|c| c.is_ascii_digit()) {
p = p
.saturating_mul(10)
.saturating_add((*d as u8 - b'0') as usize);
i += 1;
}
prec = Some(p);
}
}
while bytes.get(i) == Some(&'l') {
i += 1;
}
let Some(&conv) = bytes.get(i) else { break };
i += 1;
let next = |idx: &mut usize| -> Value {
let v = args.get(*idx).cloned().unwrap_or(Value::Null);
*idx += 1;
v
};
if width >= PRINTF_LIMIT || prec.is_some_and(|p| p >= PRINTF_LIMIT) {
too_big = true;
break;
}
let numeric = matches!(
conv,
'd' | 'i' | 'u' | 'x' | 'X' | 'o' | 'f' | 'e' | 'E' | 'g' | 'G'
);
let body = match conv {
'd' | 'i' => int_body(eval::to_i64(&next(&mut arg_idx)), prec, plus, space),
'u' => int_unsigned(
eval::to_i64(&next(&mut arg_idx)) as u64,
10,
false,
prec,
alt,
),
'x' => int_unsigned(
eval::to_i64(&next(&mut arg_idx)) as u64,
16,
false,
prec,
alt,
),
'X' => int_unsigned(
eval::to_i64(&next(&mut arg_idx)) as u64,
16,
true,
prec,
alt,
),
'o' => int_unsigned(
eval::to_i64(&next(&mut arg_idx)) as u64,
8,
false,
prec,
alt,
),
'c' => {
let v = next(&mut arg_idx);
if matches!(v, Value::Null) {
String::from('\0')
} else {
eval::to_text(&v)
.chars()
.next()
.map(String::from)
.unwrap_or_default()
}
}
'f' => {
let f = eval::to_f64(&next(&mut arg_idx));
let f = if f == 0.0 { 0.0 } else { f };
if !f.is_finite() {
nonfinite(f, plus, space)
} else {
let p = prec.unwrap_or(6);
let mut s = fixed(f, p, bang);
if alt && p == 0 && !s.contains('.') {
s.push('.');
}
if bang {
s = strip_zeros_keep_one(&s);
}
with_sign(s, plus, space)
}
}
'e' | 'E' => {
let f = eval::to_f64(&next(&mut arg_idx));
let f = if f == 0.0 { 0.0 } else { f };
if !f.is_finite() {
nonfinite(f, plus, space)
} else {
let mut s = fmt_exp(f, prec.unwrap_or(6), conv == 'E', bang);
if bang {
s = strip_zeros_keep_one(&s);
}
with_sign(s, plus, space)
}
}
'g' | 'G' => {
let f = eval::to_f64(&next(&mut arg_idx));
if !f.is_finite() {
nonfinite(f, plus, space)
} else {
with_sign(
fmt_general(f, prec.unwrap_or(6), conv == 'G', alt, bang),
plus,
space,
)
}
}
's' | 'z' => {
let v = next(&mut arg_idx);
let mut s = eval::to_text(&v);
if let Some(pr) = prec {
s = s.chars().take(pr).collect();
}
s
}
'q' => {
let v = next(&mut arg_idx);
let mut s = match v {
Value::Null => String::from("(NULL)"),
other => eval::to_text(&other).replace('\'', "''"),
};
if let Some(pr) = prec {
s = s.chars().take(pr).collect();
}
s
}
'Q' => {
let v = next(&mut arg_idx);
let mut s = match v {
Value::Null => String::from("NULL"),
other => alloc::format!("'{}'", eval::to_text(&other).replace('\'', "''")),
};
if let Some(pr) = prec {
s = s.chars().take(pr).collect();
}
s
}
'w' => {
let v = next(&mut arg_idx);
let mut s = match v {
Value::Null => String::from("(NULL)"),
other => eval::to_text(&other).replace('"', "\"\""),
};
if let Some(pr) = prec {
s = s.chars().take(pr).collect();
}
s
}
_ => {
break;
}
};
let zero_pad = zero && numeric;
let grouped = comma && matches!(conv, 'd' | 'i' | 'f');
let body = if grouped && zero_pad && width > body.chars().count() {
let (sign, mag) = match body.strip_prefix(['+', '-', ' ']) {
Some(r) => (&body[..1], r),
None => ("", body.as_str()),
};
let pad = width - body.chars().count();
let padded = alloc::format!("{sign}{}{mag}", "0".repeat(pad));
group_thousands(&padded)
} else if grouped {
group_thousands(&body)
} else {
body
};
let zero_pad = zero_pad && !grouped;
let is_int = matches!(conv, 'd' | 'i' | 'u' | 'x' | 'X' | 'o');
let zero_wins = zero_pad && (is_int || !left);
let alt_prefix_len = if alt && zero_pad {
match conv {
'x' | 'X' if body.starts_with("0x") || body.starts_with("0X") => 2,
'o' if body.starts_with('0') => 1,
_ => 0,
}
} else {
0
};
let len = body.chars().count();
let effective = if zero_wins { len - alt_prefix_len } else { len };
if width > effective {
if (out.len() + width + alt_prefix_len) >= PRINTF_LIMIT {
too_big = true;
break;
}
let pad = width - effective;
if zero_wins {
let rest = body.as_str();
let (sign, rest) = if let Some(r) = rest.strip_prefix(['-', '+', ' ']) {
(&body[..1], r)
} else {
("", rest)
};
let (prefix, rest) = rest.split_at(alt_prefix_len.min(rest.len()));
out.push_str(sign);
out.push_str(prefix);
for _ in 0..pad {
out.push('0');
}
out.push_str(rest);
} else if left {
out.push_str(&body);
for _ in 0..pad {
out.push(' ');
}
} else {
for _ in 0..pad {
out.push(' ');
}
out.push_str(&body);
}
} else {
out.push_str(&body);
}
if out.len() >= PRINTF_LIMIT {
too_big = true;
break;
}
}
if too_big {
return Value::Text(String::new());
}
Value::Text(out)
}
fn fixed(f: f64, p: usize, bang: bool) -> String {
if !bang && f.is_finite() && f != 0.0 {
let sd = shortest_sig_count(f);
let kshort = ((sd as i32 - 1) - lead_exp10(f)).max(0) as usize;
if p > kshort {
let r = crate::exec::func::round_half_away(f, kshort as u32);
let mut s = alloc::format!("{r:.kshort$}");
if kshort == 0 {
s.push('.');
}
for _ in 0..(p - kshort) {
s.push('0');
}
return s;
}
}
const CAP: usize = 1100;
if p <= CAP {
let r = crate::exec::func::round_half_away(f, p as u32);
return alloc::format!("{r:.p$}");
}
let r = crate::exec::func::round_half_away(f, CAP as u32);
let head = alloc::format!("{r:.CAP$}");
let mut s = String::with_capacity(head.len() + (p - CAP));
s.push_str(&head);
for _ in 0..(p - CAP) {
s.push('0');
}
s
}
fn nonfinite(f: f64, plus: bool, space: bool) -> String {
if f.is_nan() {
return String::from("NaN");
}
if f < 0.0 {
String::from("-Inf")
} else {
with_sign(String::from("Inf"), plus, space)
}
}
fn int_body(n: i64, prec: Option<usize>, plus: bool, space: bool) -> String {
let mag = n.unsigned_abs();
let mut digits = alloc::format!("{mag}");
if let Some(p) = prec {
while digits.len() < p {
digits.insert(0, '0');
}
}
let mut s = digits;
if n < 0 {
s.insert(0, '-');
} else if plus {
s.insert(0, '+');
} else if space {
s.insert(0, ' ');
}
s
}
fn int_unsigned(n: u64, base: u32, upper: bool, prec: Option<usize>, alt: bool) -> String {
let mut digits = match base {
16 if upper => alloc::format!("{n:X}"),
16 => alloc::format!("{n:x}"),
8 => alloc::format!("{n:o}"),
_ => alloc::format!("{n}"),
};
if let Some(p) = prec {
while digits.len() < p {
digits.insert(0, '0');
}
}
if alt && n != 0 {
match base {
16 if upper => digits.insert_str(0, "0X"),
16 => digits.insert_str(0, "0x"),
8 => digits.insert(0, '0'),
_ => {}
}
}
digits
}
fn with_sign(mut s: String, plus: bool, space: bool) -> String {
if !s.starts_with('-') {
if plus {
s.insert(0, '+');
} else if space {
s.insert(0, ' ');
}
}
s
}
fn fmt_general(f: f64, prec: usize, upper: bool, alt: bool, bang: bool) -> String {
let p = prec.max(1);
let shape = |s: &str| -> String {
if alt {
if s.contains('.') {
String::from(s)
} else {
alloc::format!("{s}.")
}
} else if bang {
strip_zeros_keep_one(s)
} else {
strip_trailing_zeros(s)
}
};
if f == 0.0 {
return shape(&alloc::format!("{:.*}", p - 1, 0.0));
}
let e_str = fmt_exp(f, p - 1, false, bang);
let exp: i32 = e_str
.split(['e', 'E'])
.nth(1)
.and_then(|x| x.parse().ok())
.unwrap_or(0);
if exp < -4 || exp >= p as i32 {
let upper_e = fmt_exp(f, p - 1, upper, bang);
let (mant, e) = match upper_e.find(['e', 'E']) {
Some(pos) => upper_e.split_at(pos),
None => return upper_e,
};
alloc::format!("{}{e}", shape(mant))
} else {
let frac = (p as i32 - 1 - exp).max(0) as usize;
shape(&fixed(f, frac, bang))
}
}
fn strip_trailing_zeros(s: &str) -> String {
if s.contains('.') {
let t = s.trim_end_matches('0');
String::from(t.trim_end_matches('.'))
} else {
String::from(s)
}
}
fn capped16(f: f64) -> (alloc::string::String, i32) {
let s = alloc::format!("{:.15e}", f.abs());
let (mant, exp) = s.split_once(['e', 'E']).unwrap_or((s.as_str(), "0"));
let exp: i32 = exp.parse().unwrap_or(0);
let digs: alloc::string::String = mant
.bytes()
.filter(u8::is_ascii_digit)
.map(char::from)
.collect();
let stripped = digs.trim_end_matches('0');
let stripped = if stripped.is_empty() { "0" } else { stripped };
(alloc::string::String::from(stripped), exp)
}
fn shortest_sig_count(f: f64) -> usize {
capped16(f).0.len()
}
fn lead_exp10(f: f64) -> i32 {
capped16(f).1
}
fn fmt_exp(f: f64, prec: usize, upper: bool, bang: bool) -> String {
const CAP: usize = 1100;
let s = if !bang && f.is_finite() && f != 0.0 && prec >= shortest_sig_count(f) {
let sd = shortest_sig_count(f);
let short = alloc::format!("{:.*e}", sd - 1, f);
match short.split_once(['e', 'E']) {
Some((mant, exp)) => {
let zeros = "0".repeat(prec - (sd - 1));
let mant = if mant.contains('.') {
alloc::format!("{mant}{zeros}")
} else {
alloc::format!("{mant}.{zeros}")
};
alloc::format!("{mant}e{exp}")
}
None => short,
}
} else if prec <= CAP {
alloc::format!("{:.*e}", prec, f)
} else {
let head = alloc::format!("{:.*e}", CAP, f);
match head.find(['e', 'E']) {
Some(pos) => {
let (mant, exp) = head.split_at(pos);
alloc::format!("{mant}{}{exp}", "0".repeat(prec - CAP))
}
None => head,
}
};
if let Some(pos) = s.find(['e', 'E']) {
let (mantissa, exp) = s.split_at(pos);
let exp = &exp[1..];
let (sign, digits) = match exp.strip_prefix('-') {
Some(d) => ('-', d),
None => ('+', exp.strip_prefix('+').unwrap_or(exp)),
};
let e = if upper { 'E' } else { 'e' };
alloc::format!("{mantissa}{e}{sign}{:0>2}", digits)
} else {
s
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
fn t(v: &str) -> Value {
Value::Text(String::from(v))
}
#[test]
fn basic_date() {
assert_eq!(date(&[t("2000-01-01")]), t("2000-01-01"));
assert_eq!(
datetime(&[t("2000-01-01 12:34:56")]),
t("2000-01-01 12:34:56")
);
assert_eq!(time(&[t("2000-01-01 12:34:56")]), t("12:34:56"));
}
#[test]
fn modifiers() {
assert_eq!(date(&[t("2000-01-01"), t("+1 day")]), t("2000-01-02"));
assert_eq!(date(&[t("2000-01-31"), t("+1 month")]), t("2000-03-02"));
assert_eq!(date(&[t("2000-01-01"), t("+1 year")]), t("2001-01-01"));
assert_eq!(
date(&[t("2000-01-15"), t("start of month")]),
t("2000-01-01")
);
}
#[test]
fn unixepoch_modifier() {
assert_eq!(
datetime(&[Value::Integer(0), t("unixepoch")]),
t("1970-01-01 00:00:00")
);
assert_eq!(unixepoch(&[t("1970-01-01 00:00:00")]), Value::Integer(0));
}
#[test]
fn timediff_basic() {
let td = |a: &str, b: &str| timediff(&t(a), &t(b));
assert_eq!(
td("2020-01-02 00:00:00", "2020-01-01 00:00:00"),
t("+0000-00-01 00:00:00.000")
);
assert_eq!(
td("2024-03-01", "2024-02-01"),
t("+0000-01-00 00:00:00.000")
);
assert_eq!(
td("2020-01-01", "2020-03-01"),
t("-0000-02-00 00:00:00.000")
);
assert_eq!(
td("2025-06-15", "2020-01-01"),
t("+0005-05-14 00:00:00.000")
);
assert_eq!(
td("2020-01-01 12:30:45.5", "2020-01-01 12:00:00"),
t("+0000-00-00 00:30:45.500")
);
assert_eq!(
td("2020-01-01", "2020-01-01"),
t("+0000-00-00 00:00:00.000")
);
assert_eq!(
td("2024-03-31", "2024-02-29"),
t("+0000-01-02 00:00:00.000")
);
assert_eq!(
td("2159-10-07", "2156-03-17"),
t("+0003-06-20 00:00:00.000")
);
assert_eq!(
td("2156-03-17", "2159-10-07"),
t("-0003-06-21 00:00:00.000")
);
assert_eq!(timediff(&t("not a date"), &t("2020-01-01")), Value::Null);
assert_eq!(timediff(&Value::Null, &t("2020-01-01")), Value::Null);
}
#[test]
fn strftime_codes() {
assert_eq!(strftime(&[t("%Y/%m/%d"), t("2000-01-02")]), t("2000/01/02"));
assert_eq!(
strftime(&[t("%H:%M:%S"), t("2000-01-02 03:04:05")]),
t("03:04:05")
);
}
#[test]
fn printf_basic() {
assert_eq!(
printf(&[t("%d-%d"), Value::Integer(1), Value::Integer(2)]),
t("1-2")
);
assert_eq!(printf(&[t("%05d"), Value::Integer(42)]), t("00042"));
assert_eq!(printf(&[t("%.2f"), Value::Real(3.567)]), t("3.57"));
assert_eq!(printf(&[t("%-5d|"), Value::Integer(7)]), t("7 |"));
assert_eq!(printf(&[t("%x"), Value::Integer(255)]), t("ff"));
assert_eq!(printf(&[t("%s and %s"), t("a"), t("b")]), t("a and b"));
}
#[cfg(feature = "std")]
#[test]
fn now_returns_current_date() {
let Value::Text(s) = date(&[t("now")]) else {
panic!("date('now') should not be NULL with std");
};
assert_eq!(s.len(), 10, "got {s:?}");
let year: i32 = s[..4].parse().unwrap();
assert!(year >= 2024, "implausible year {year}");
assert_eq!(date(&[]), date(&[t("now")]));
}
#[test]
fn null_propagation() {
assert_eq!(date(&[Value::Null]), Value::Null);
assert_eq!(date(&[t("not a date")]), Value::Null);
let _ = vec![1];
}
}