use std::rc::Rc;
use rupg_common::{Error, Result, SqlState};
use rupg_types::{
FixedZone, Interval, TimeZone, USECS_PER_DAY, USECS_PER_SEC, Value, ZoneName,
decode_numeric_timezone, decode_timezone_name, interval_out, local_offset,
};
use crate::cast::{infinite, timestamp_range, valid_timestamp};
use crate::datetime::Tm;
use crate::{Call, Kernel, bad_value, type_error};
const TZ_STRLEN_MAX: usize = 255;
const UNIX_TO_POSTGRES_SECS: i64 = 946_684_800;
enum Named {
Fixed(i32),
Dynamic(Rc<dyn TimeZone>),
Zone(Rc<dyn TimeZone>),
}
fn zone_name(v: &Value) -> Result<&str> {
let Value::Text(s) = v else { return Err(bad_value()) };
let mut end = s.len().min(TZ_STRLEN_MAX);
while !s.is_char_boundary(end) {
end -= 1;
}
Ok(&s[..end])
}
fn decode(call: &Call<'_>, name: &str) -> Result<Named> {
let session = call.session.zone()?;
let cx = crate::io::datetime_input(call.session, session.as_ref());
let named = decode_timezone_name(name, &cx).map_err(type_error)?;
Ok(match named {
ZoneName::Fixed(east) => Named::Fixed(east),
ZoneName::Dynamic(None) => Named::Dynamic(Rc::clone(&session)),
ZoneName::Dynamic(Some(zone)) => Named::Dynamic(Rc::new(zone)),
ZoneName::Zone(zone) => Named::Zone(Rc::new(zone)),
})
}
pub(crate) fn lookup_timezone(call: &Call<'_>, zone: &Value) -> Result<Rc<dyn TimeZone>> {
Ok(match decode(call, zone_name(zone)?)? {
Named::Fixed(offset) => Rc::new(FixedZone { offset, abbrev: String::new() }),
Named::Dynamic(zone) | Named::Zone(zone) => zone,
})
}
pub(crate) fn parse_sane_timezone(call: &Call<'_>, local: i64, zone: &Value) -> Result<i32> {
let name = zone_name(zone)?;
if name.starts_with(|c: char| c.is_ascii_digit()) {
return Err(Error::new(
SqlState::INVALID_PARAMETER_VALUE,
format!("invalid input syntax for type numeric time zone: \"{name}\""),
)
.with_hint("Numeric time zones must have \"-\" or \"+\" as first character."));
}
if let Some(west) = decode_numeric_timezone(name).map_err(type_error)? {
return Ok(west);
}
Ok(match decode(call, name)? {
Named::Fixed(east) => -east,
Named::Dynamic(zone) => abbrev_offset(local, name, zone.as_ref()),
Named::Zone(zone) => local_offset(whole_seconds(local), zone.as_ref()).0,
})
}
fn time_t(ts: i64) -> i64 {
ts / USECS_PER_SEC + UNIX_TO_POSTGRES_SECS
}
fn whole_seconds(ts: i64) -> i64 {
ts.div_euclid(USECS_PER_SEC) + UNIX_TO_POSTGRES_SECS
}
fn east_at(zone: &dyn TimeZone, ts: i64) -> i32 {
zone.at(ts.div_euclid(USECS_PER_SEC) + UNIX_TO_POSTGRES_SECS).0
}
fn local_in(zone: &dyn TimeZone, ts: i64) -> Result<i64> {
Tm::of(ts)?;
let local =
ts.checked_add(i64::from(east_at(zone, ts)) * USECS_PER_SEC).ok_or_else(timestamp_range)?;
Tm::of(local)?;
Ok(local)
}
fn abbrev_offset(local: i64, abbrev: &str, zone: &dyn TimeZone) -> i32 {
let (west, instant) = local_offset(whole_seconds(local), zone);
match zone.abbrev_at(&abbrev.to_ascii_uppercase(), instant) {
Some((east, _)) => -east,
None => west,
}
}
fn abbrev_offset_ts(ts: i64, abbrev: &str, zone: &dyn TimeZone) -> Result<i32> {
if let Some((east, _)) = zone.abbrev_at(&abbrev.to_ascii_uppercase(), time_t(ts)) {
return Ok(-east);
}
let local = local_in(zone, ts)?;
Ok(local_offset(whole_seconds(local), zone).0)
}
fn interval_zone(call: &Call<'_>, v: &Value) -> Result<i32> {
let Value::Interval(zone) = v else { return Err(bad_value()) };
let problem = if *zone == Interval::INFINITY || *zone == Interval::NEGATIVE_INFINITY {
Some("must be finite")
} else if zone.month != 0 || zone.day != 0 {
Some("must not include months or days")
} else {
None
};
if let Some(problem) = problem {
let mut text = Vec::new();
interval_out(zone, call.session.interval_style(), &mut text);
let text = String::from_utf8_lossy(&text);
return Err(Error::new(
SqlState::INVALID_PARAMETER_VALUE,
format!("interval time zone \"{text}\" {problem}"),
));
}
#[allow(clippy::cast_possible_truncation)]
let east = (zone.time / USECS_PER_SEC) as i32;
Ok(east.wrapping_neg())
}
fn dt2local(ts: i64, west: i32) -> i64 {
ts.wrapping_sub(i64::from(west).wrapping_mul(USECS_PER_SEC))
}
fn checked(ts: i64) -> Result<i64> {
if valid_timestamp(ts) { Ok(ts) } else { Err(timestamp_range()) }
}
fn timestamp_zone(call: &Call<'_>, args: &[Value]) -> Result<Value> {
let [zone, Value::Timestamp(ts)] = args else { return Err(bad_value()) };
if infinite(*ts) {
return Ok(Value::TimestampTz(*ts));
}
let name = zone_name(zone)?;
let result = match decode(call, name)? {
Named::Fixed(east) => dt2local(*ts, east),
Named::Dynamic(zone) => {
Tm::of(*ts)?;
dt2local(*ts, -abbrev_offset(*ts, name, zone.as_ref()))
}
Named::Zone(zone) => Tm::of(*ts)?.timestamp(Some(zone.as_ref()))?,
};
checked(result).map(Value::TimestampTz)
}
fn timestamp_izone(call: &Call<'_>, args: &[Value]) -> Result<Value> {
let [zone, Value::Timestamp(ts)] = args else { return Err(bad_value()) };
if infinite(*ts) {
return Ok(Value::TimestampTz(*ts));
}
let west = interval_zone(call, zone)?;
checked(dt2local(*ts, west.wrapping_neg())).map(Value::TimestampTz)
}
fn timestamptz_zone(call: &Call<'_>, args: &[Value]) -> Result<Value> {
let [zone, Value::TimestampTz(ts)] = args else { return Err(bad_value()) };
if infinite(*ts) {
return Ok(Value::Timestamp(*ts));
}
let name = zone_name(zone)?;
let result = match decode(call, name)? {
Named::Fixed(east) => dt2local(*ts, -east),
Named::Dynamic(zone) => dt2local(*ts, abbrev_offset_ts(*ts, name, zone.as_ref())?),
Named::Zone(zone) => Tm::of(local_in(zone.as_ref(), *ts)?)?.timestamp(None)?,
};
checked(result).map(Value::Timestamp)
}
fn timestamptz_izone(call: &Call<'_>, args: &[Value]) -> Result<Value> {
let [zone, Value::TimestampTz(ts)] = args else { return Err(bad_value()) };
if infinite(*ts) {
return Ok(Value::Timestamp(*ts));
}
let west = interval_zone(call, zone)?;
checked(dt2local(*ts, west)).map(Value::Timestamp)
}
fn rotate(time: i64, zone: i32, west: i32) -> Value {
let mut time = time + i64::from(zone - west) * USECS_PER_SEC;
while time < 0 {
time += USECS_PER_DAY;
}
if time >= USECS_PER_DAY {
time %= USECS_PER_DAY;
}
Value::TimeTz(time, west)
}
fn timetz_zone(call: &Call<'_>, args: &[Value]) -> Result<Value> {
let [zone, Value::TimeTz(time, offset)] = args else { return Err(bad_value()) };
timetz_in_zone(call, zone_name(zone)?, *time, *offset)
}
fn timetz_in_zone(call: &Call<'_>, name: &str, time: i64, offset: i32) -> Result<Value> {
let west = match decode(call, name)? {
Named::Fixed(east) => -east,
Named::Dynamic(zone) => {
abbrev_offset_ts(call.session.transaction_start(), name, zone.as_ref())?
}
Named::Zone(zone) => -east_at(zone.as_ref(), call.session.transaction_start()),
};
Ok(rotate(time, offset, west))
}
fn timetz_izone(call: &Call<'_>, args: &[Value]) -> Result<Value> {
let [zone, Value::TimeTz(time, offset)] = args else { return Err(bad_value()) };
let west = interval_zone(call, zone)?;
Ok(rotate(*time, *offset, west))
}
fn timetz_at_local(call: &Call<'_>, args: &[Value]) -> Result<Value> {
let [Value::TimeTz(time, offset)] = args else { return Err(bad_value()) };
let name = call.session.setting("timezone").ok_or_else(bad_value)?;
timetz_in_zone(call, &name, *time, *offset)
}
pub(crate) fn by_src(src: &str) -> Option<Kernel> {
Some(match src {
"timestamp_zone" => timestamp_zone,
"timestamp_izone" => timestamp_izone,
"timestamptz_zone" => timestamptz_zone,
"timestamptz_izone" => timestamptz_izone,
"timetz_zone" => timetz_zone,
"timetz_izone" => timetz_izone,
"timetz_at_local" => timetz_at_local,
_ => return None,
})
}