use std::collections::BTreeMap;
use std::hash::{Hash, Hasher};
use chrono::Datelike;
use rust_decimal::Decimal;
use rustledger_core::{Amount, Inventory, Metadata, NaiveDate, Position, Transaction};
#[derive(Debug, Clone)]
pub struct SourceLocation {
pub filename: String,
pub lineno: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum IntervalUnit {
Day,
Week,
Month,
Quarter,
Year,
}
impl IntervalUnit {
pub fn parse_unit(s: &str) -> Option<Self> {
match s.to_uppercase().as_str() {
"DAY" | "DAYS" | "D" => Some(Self::Day),
"WEEK" | "WEEKS" | "W" => Some(Self::Week),
"MONTH" | "MONTHS" | "M" => Some(Self::Month),
"QUARTER" | "QUARTERS" | "Q" => Some(Self::Quarter),
"YEAR" | "YEARS" | "Y" => Some(Self::Year),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Interval {
pub count: i64,
pub unit: IntervalUnit,
}
impl Interval {
pub const fn new(count: i64, unit: IntervalUnit) -> Self {
Self { count, unit }
}
pub(crate) const fn to_approx_days(&self) -> i64 {
let days_per_unit = match self.unit {
IntervalUnit::Day => 1,
IntervalUnit::Week => 7,
IntervalUnit::Month => 30,
IntervalUnit::Quarter => 91,
IntervalUnit::Year => 365,
};
self.count.saturating_mul(days_per_unit)
}
#[allow(clippy::missing_const_for_fn)] pub fn add_to_date(&self, date: NaiveDate) -> Option<NaiveDate> {
use chrono::Months;
match self.unit {
IntervalUnit::Day => date.checked_add_signed(chrono::Duration::days(self.count)),
IntervalUnit::Week => date.checked_add_signed(chrono::Duration::weeks(self.count)),
IntervalUnit::Month => {
if self.count >= 0 {
date.checked_add_months(Months::new(self.count as u32))
} else {
date.checked_sub_months(Months::new((-self.count) as u32))
}
}
IntervalUnit::Quarter => {
let months = self.count * 3;
if months >= 0 {
date.checked_add_months(Months::new(months as u32))
} else {
date.checked_sub_months(Months::new((-months) as u32))
}
}
IntervalUnit::Year => {
let months = self.count * 12;
if months >= 0 {
date.checked_add_months(Months::new(months as u32))
} else {
date.checked_sub_months(Months::new((-months) as u32))
}
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Value {
String(String),
Number(Decimal),
Integer(i64),
Date(NaiveDate),
Boolean(bool),
Amount(Amount),
Position(Box<Position>),
Inventory(Box<Inventory>),
StringSet(Vec<String>),
Metadata(Box<Metadata>),
Interval(Interval),
Object(Box<BTreeMap<String, Self>>),
Null,
}
impl Value {
pub(crate) fn hash_value<H: Hasher>(&self, state: &mut H) {
std::mem::discriminant(self).hash(state);
match self {
Self::String(s) => s.hash(state),
Self::Number(d) => d.serialize().hash(state),
Self::Integer(i) => i.hash(state),
Self::Date(d) => {
d.year().hash(state);
d.month().hash(state);
d.day().hash(state);
}
Self::Boolean(b) => b.hash(state),
Self::Amount(a) => {
a.number.serialize().hash(state);
a.currency.as_str().hash(state);
}
Self::Position(p) => {
p.units.number.serialize().hash(state);
p.units.currency.as_str().hash(state);
if let Some(cost) = &p.cost {
cost.number.serialize().hash(state);
cost.currency.as_str().hash(state);
}
}
Self::Inventory(inv) => {
for pos in inv.positions() {
pos.units.number.serialize().hash(state);
pos.units.currency.as_str().hash(state);
if let Some(cost) = &pos.cost {
cost.number.serialize().hash(state);
cost.currency.as_str().hash(state);
}
}
}
Self::StringSet(ss) => {
let mut sorted = ss.clone();
sorted.sort();
for s in &sorted {
s.hash(state);
}
}
Self::Metadata(meta) => {
let mut keys: Vec<_> = meta.keys().collect();
keys.sort();
for key in keys {
key.hash(state);
format!("{:?}", meta.get(key)).hash(state);
}
}
Self::Interval(interval) => {
interval.count.hash(state);
interval.unit.hash(state);
}
Self::Object(obj) => {
for (k, v) in obj.as_ref() {
k.hash(state);
v.hash_value(state);
}
}
Self::Null => {}
}
}
}
pub type Row = Vec<Value>;
pub fn hash_row(row: &Row) -> u64 {
use std::collections::hash_map::DefaultHasher;
let mut hasher = DefaultHasher::new();
for value in row {
value.hash_value(&mut hasher);
}
hasher.finish()
}
pub fn hash_single_value(value: &Value) -> u64 {
use std::collections::hash_map::DefaultHasher;
let mut hasher = DefaultHasher::new();
value.hash_value(&mut hasher);
hasher.finish()
}
#[derive(Debug, Clone)]
pub struct QueryResult {
pub columns: Vec<String>,
pub rows: Vec<Row>,
}
impl QueryResult {
pub const fn new(columns: Vec<String>) -> Self {
Self {
columns,
rows: Vec::new(),
}
}
pub fn add_row(&mut self, row: Row) {
self.rows.push(row);
}
pub const fn len(&self) -> usize {
self.rows.len()
}
pub const fn is_empty(&self) -> bool {
self.rows.is_empty()
}
}
#[derive(Debug)]
pub struct PostingContext<'a> {
pub transaction: &'a Transaction,
pub posting_index: usize,
pub balance: Option<Inventory>,
pub directive_index: Option<usize>,
}
#[derive(Debug, Clone)]
pub struct WindowContext {
pub row_number: usize,
pub rank: usize,
pub dense_rank: usize,
}
#[derive(Debug, Clone)]
pub struct AccountInfo {
pub open_date: Option<NaiveDate>,
pub close_date: Option<NaiveDate>,
pub open_meta: Metadata,
}
#[derive(Debug, Clone)]
pub struct Table {
pub columns: Vec<String>,
pub rows: Vec<Vec<Value>>,
}
impl Table {
#[allow(clippy::missing_const_for_fn)] pub fn new(columns: Vec<String>) -> Self {
Self {
columns,
rows: Vec::new(),
}
}
pub fn add_row(&mut self, row: Vec<Value>) {
self.rows.push(row);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_value_size() {
use std::mem::size_of;
assert!(
size_of::<Value>() <= 48,
"Value enum too large: {} bytes",
size_of::<Value>()
);
}
}