use crate::error::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OracleDate {
pub year: i32,
pub month: u8,
pub day: u8,
pub hour: u8,
pub minute: u8,
pub second: u8,
}
impl OracleDate {
pub fn new(year: i32, month: u8, day: u8, hour: u8, minute: u8, second: u8) -> Self {
Self {
year,
month,
day,
hour,
minute,
second,
}
}
pub fn date(year: i32, month: u8, day: u8) -> Self {
Self::new(year, month, day, 0, 0, 0)
}
pub fn to_oracle_bytes(&self) -> [u8; 7] {
let century = (self.year / 100) as u8 + 100;
let year_in_century = (self.year % 100) as u8 + 100;
[
century,
year_in_century,
self.month,
self.day,
self.hour + 1,
self.minute + 1,
self.second + 1,
]
}
}
impl Default for OracleDate {
fn default() -> Self {
Self::new(1, 1, 1, 0, 0, 0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OracleTimestamp {
pub year: i32,
pub month: u8,
pub day: u8,
pub hour: u8,
pub minute: u8,
pub second: u8,
pub microsecond: u32,
pub tz_hour_offset: i8,
pub tz_minute_offset: i8,
}
impl OracleTimestamp {
pub fn new(
year: i32,
month: u8,
day: u8,
hour: u8,
minute: u8,
second: u8,
microsecond: u32,
) -> Self {
Self {
year,
month,
day,
hour,
minute,
second,
microsecond,
tz_hour_offset: 0,
tz_minute_offset: 0,
}
}
pub fn with_timezone(
year: i32,
month: u8,
day: u8,
hour: u8,
minute: u8,
second: u8,
microsecond: u32,
tz_hour_offset: i8,
tz_minute_offset: i8,
) -> Self {
Self {
year,
month,
day,
hour,
minute,
second,
microsecond,
tz_hour_offset,
tz_minute_offset,
}
}
pub fn has_timezone(&self) -> bool {
self.tz_hour_offset != 0 || self.tz_minute_offset != 0
}
pub fn to_date(&self) -> OracleDate {
OracleDate::new(
self.year,
self.month,
self.day,
self.hour,
self.minute,
self.second,
)
}
pub fn to_oracle_bytes(&self) -> [u8; 11] {
let date_bytes = self.to_date().to_oracle_bytes();
let nanos = self.microsecond * 1000;
let nano_bytes = nanos.to_be_bytes();
[
date_bytes[0],
date_bytes[1],
date_bytes[2],
date_bytes[3],
date_bytes[4],
date_bytes[5],
date_bytes[6],
nano_bytes[0],
nano_bytes[1],
nano_bytes[2],
nano_bytes[3],
]
}
}
impl Default for OracleTimestamp {
fn default() -> Self {
Self::new(1, 1, 1, 0, 0, 0, 0)
}
}
impl From<OracleDate> for OracleTimestamp {
fn from(date: OracleDate) -> Self {
Self::new(
date.year,
date.month,
date.day,
date.hour,
date.minute,
date.second,
0,
)
}
}
const TZ_HOUR_OFFSET: i8 = 20;
const TZ_MINUTE_OFFSET: i8 = 60;
const HAS_REGION_ID: u8 = 0x80;
pub fn decode_oracle_date(data: &[u8]) -> Result<OracleDate> {
if data.len() < 7 {
return Err(Error::DataConversionError(format!(
"Oracle DATE requires 7 bytes, got {}",
data.len()
)));
}
let century = data[0] as i32 - 100;
let year_in_century = data[1] as i32 - 100;
let year = century * 100 + year_in_century;
Ok(OracleDate {
year,
month: data[2],
day: data[3],
hour: data[4].saturating_sub(1),
minute: data[5].saturating_sub(1),
second: data[6].saturating_sub(1),
})
}
pub fn decode_oracle_timestamp(data: &[u8]) -> Result<OracleTimestamp> {
if data.len() < 7 {
return Err(Error::DataConversionError(format!(
"Oracle TIMESTAMP requires at least 7 bytes, got {}",
data.len()
)));
}
let date = decode_oracle_date(data)?;
let fsecond = if data.len() >= 11 {
let nanos = u32::from_be_bytes([data[7], data[8], data[9], data[10]]);
nanos / 1000 } else {
0
};
let (tz_hour, tz_minute) = if data.len() >= 13 && data[11] != 0 && data[12] != 0 {
if data[11] & HAS_REGION_ID != 0 {
return Err(Error::DataConversionError(
"Named timezone regions are not supported".to_string(),
));
}
(
(data[11] as i8) - TZ_HOUR_OFFSET,
(data[12] as i8) - TZ_MINUTE_OFFSET,
)
} else {
(0, 0)
};
Ok(OracleTimestamp {
year: date.year,
month: date.month,
day: date.day,
hour: date.hour,
minute: date.minute,
second: date.second,
microsecond: fsecond,
tz_hour_offset: tz_hour,
tz_minute_offset: tz_minute,
})
}
pub fn encode_oracle_date(date: &OracleDate) -> Vec<u8> {
let century = (date.year / 100) as u8 + 100;
let year_in_century = (date.year % 100) as u8 + 100;
vec![
century,
year_in_century,
date.month,
date.day,
date.hour + 1,
date.minute + 1,
date.second + 1,
]
}
pub fn encode_oracle_timestamp(ts: &OracleTimestamp, include_tz: bool) -> Vec<u8> {
let date = ts.to_date();
let mut result = encode_oracle_date(&date);
if ts.microsecond > 0 || include_tz {
let nanos = ts.microsecond * 1000;
result.extend_from_slice(&nanos.to_be_bytes());
}
if include_tz {
result.push((ts.tz_hour_offset + TZ_HOUR_OFFSET) as u8);
result.push((ts.tz_minute_offset + TZ_MINUTE_OFFSET) as u8);
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_decode_date() {
let data = vec![
120, 124, 3, 15, 15, 31, 46, ];
let date = decode_oracle_date(&data).unwrap();
assert_eq!(date.year, 2024);
assert_eq!(date.month, 3);
assert_eq!(date.day, 15);
assert_eq!(date.hour, 14);
assert_eq!(date.minute, 30);
assert_eq!(date.second, 45);
}
#[test]
fn test_encode_date() {
let date = OracleDate::new(2024, 3, 15, 14, 30, 45);
let encoded = encode_oracle_date(&date);
let decoded = decode_oracle_date(&encoded).unwrap();
assert_eq!(decoded.year, date.year);
assert_eq!(decoded.month, date.month);
assert_eq!(decoded.day, date.day);
assert_eq!(decoded.hour, date.hour);
assert_eq!(decoded.minute, date.minute);
assert_eq!(decoded.second, date.second);
}
#[test]
fn test_date_roundtrip() {
let original = OracleDate::new(1999, 12, 31, 23, 59, 59);
let encoded = encode_oracle_date(&original);
let decoded = decode_oracle_date(&encoded).unwrap();
assert_eq!(original, decoded);
}
#[test]
fn test_decode_timestamp_with_fractional() {
let mut data = vec![120, 124, 3, 15, 15, 31, 46];
let nanos: u32 = 123456000;
data.extend_from_slice(&nanos.to_be_bytes());
let ts = decode_oracle_timestamp(&data).unwrap();
assert_eq!(ts.year, 2024);
assert_eq!(ts.microsecond, 123456);
}
#[test]
fn test_decode_timestamp_with_timezone() {
let mut data = vec![120, 124, 3, 15, 15, 31, 46];
data.extend_from_slice(&[0, 0, 0, 0]); data.push(25); data.push(90);
let ts = decode_oracle_timestamp(&data).unwrap();
assert_eq!(ts.tz_hour_offset, 5);
assert_eq!(ts.tz_minute_offset, 30);
assert!(ts.has_timezone());
}
#[test]
fn test_timestamp_to_date_conversion() {
let ts = OracleTimestamp::new(2024, 3, 15, 14, 30, 45, 123456);
let date = ts.to_date();
assert_eq!(date.year, 2024);
assert_eq!(date.month, 3);
assert_eq!(date.day, 15);
assert_eq!(date.hour, 14);
assert_eq!(date.minute, 30);
assert_eq!(date.second, 45);
}
#[test]
fn test_date_to_timestamp_conversion() {
let date = OracleDate::new(2024, 3, 15, 14, 30, 45);
let ts: OracleTimestamp = date.into();
assert_eq!(ts.year, 2024);
assert_eq!(ts.microsecond, 0);
assert!(!ts.has_timezone());
}
#[test]
fn test_negative_year() {
let data = vec![
99, 100, 1,
1,
1,
1,
1,
];
let date = decode_oracle_date(&data).unwrap();
assert_eq!(date.year, -100);
}
}