Skip to content

Commit

Permalink
Merge 'RAGIB/price-data' into tep/tep-1015-price-data
Browse files Browse the repository at this point in the history
GH-40

Signed-off-by: 35V LG84 <[email protected]>
  • Loading branch information
35VLG84 committed Jan 10, 2025
2 parents 8ead932 + 26d4e51 commit 7f0ade5
Show file tree
Hide file tree
Showing 9 changed files with 271 additions and 10 deletions.
5 changes: 0 additions & 5 deletions docs/tep/tep-1015.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,6 @@ This TEP does not propose any method for calculating the fixed conversions (e.g.

No change would be made to the journal file format. A new file type would be added with a format similar to the default format of Ledger CLI as described https://ledger-cli.org/doc/ledger3.html[here].

----
'P' TIMESTAMP AMOUNT COMMODITY '=' AMOUNT COMMODITY OPTIONAL_COMMENT
----

COMMENT: This should/could be just simplified ledger format?
----
'P' TIMESTAMP COMMODITY AMOUNT COMMODITY OPT_COMMENT
----
Expand Down
1 change: 1 addition & 0 deletions tackler-core/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ pub use txn_data::TxnSet;
pub(crate) mod account_tree_node;
pub(crate) mod balance_tree_node;
pub mod posting;
pub mod price_entry;
mod register;
pub mod transaction;
pub mod txn_data;
Expand Down
49 changes: 49 additions & 0 deletions tackler-core/src/model/price_entry.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2024-2025 E257.FI and Muhammad Ragib Hasin
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

use rust_decimal::Decimal;
use std::{fmt::Write, sync::Arc};
use time::OffsetDateTime;
use winnow::{seq, PResult, Parser};

use crate::parser::parts::timestamp::parse_timestamp;
// use crate::parser::parts::txn_comment::parse_txn_comment;
// use crate::parser::parts::txn_header_code::parse_txn_code;
// use crate::parser::parts::txn_header_desc::parse_txn_description;
// use crate::parser::parts::txn_metadata::{parse_txn_meta, TxnMeta};
use crate::parser::{from_error, make_semantic_error, Stream};
use tackler_api::txn_header::{Comments, TxnHeader};
use winnow::ascii::{line_ending, space1};
use winnow::combinator::{cut_err, opt, preceded, repeat};
use winnow::error::{StrContext, StrContextValue};

use super::Commodity;

/// Entry in the price database
#[derive(Debug)]
pub struct PriceEntry {
/// Timestamp with Zone information
pub timestamp: jiff::Zoned,
/// The commodity for which price is being noted
pub base_commodity: Arc<Commodity>,
/// Price of base in _eq_ commodity
pub eq_amount: Decimal,
/// The equivalence commodity in which price is being noted
pub eq_commodity: Arc<Commodity>,
/// Comments
pub comments: Option<String>,
}
1 change: 1 addition & 0 deletions tackler-core/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub use crate::parser::tackler_txns::GitInputSelector;
use winnow::error::{ErrMode, FromExternalError};

mod error;
mod pricedb_parser;
mod tackler_parser;
mod tackler_txns;

Expand Down
1 change: 1 addition & 0 deletions tackler-core/src/parser/parts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ mod comment;
pub(crate) mod identifier;
pub(crate) mod number;
mod posting_value;
pub(super) mod pricedb;
pub(crate) mod timestamp;
mod txn_comment;
mod txn_header;
Expand Down
13 changes: 9 additions & 4 deletions tackler-core/src/parser/parts/comment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,21 @@
*/

use crate::parser::Stream;
use winnow::ascii::till_line_ending;
use winnow::stream::AsChar;
use winnow::token::one_of;
use winnow::{
ascii::till_line_ending,
error::{StrContext, StrContextValue},
};
use winnow::{seq, PResult, Parser};

pub(crate) fn p_comment<'s>(is: &mut Stream<'s>) -> PResult<&'s str> {
let m = seq!(
_: ';',
// this can not be space1 as we must preserve space for equity and identity reports
_: one_of(AsChar::is_space),
_: (
';',
// this can not be space1 as we must preserve space for equity and identity reports
one_of(AsChar::is_space)
).context(StrContext::Expected(StrContextValue::Description("comment begins with a `;` and a space character"))),
till_line_ending,
)
.parse_next(is)?;
Expand Down
101 changes: 101 additions & 0 deletions tackler-core/src/parser/parts/pricedb.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Copyright 2024-2025 E257.FI and Muhammad Ragib Hasin
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

use winnow::{
ascii::{line_ending, space0, space1},
combinator::opt,
error::{StrContext, StrContextValue},
seq, PResult, Parser,
};

use crate::model::price_entry::PriceEntry;
use crate::parser::{from_error, parts::timestamp::parse_timestamp, Stream};

use super::{comment::p_comment, identifier::p_identifier, number::p_number};

#[allow(clippy::type_complexity)]
pub(crate) fn parse_price_entry(is: &mut Stream<'_>) -> PResult<PriceEntry> {
let (timestamp, base_commodity, eq_amount, eq_commodity, comments) = seq!(
_: 'P'.context(StrContext::Expected(StrContextValue::Description("price entry starts with `P`"))),
_: space1,
parse_timestamp,
_: space1,
p_identifier
.context(StrContext::Expected(StrContextValue::Description("price entry must have base commodity"))),
_: space1,
p_number
.context(StrContext::Expected(StrContextValue::Description("price entry must have equivalent amount"))),
_: space1,
p_identifier
.context(StrContext::Expected(StrContextValue::Description("price entry must have equivalent commodity"))),
_: space0,
opt(p_comment),
_: line_ending,
)
.parse_next(is)?;

let base_commodity = is
.state
.get_or_create_commodity(Some(base_commodity))
.map_err(|e| from_error(is, &*e))?;

let eq_commodity = is
.state
.get_or_create_commodity(Some(eq_commodity))
.map_err(|e| from_error(is, &*e))?;

let comments = comments.map(String::from);

Ok(PriceEntry {
timestamp,
base_commodity,
eq_amount,
eq_commodity,
comments,
})
}

#[cfg(test)]
mod tests {
use super::*;
use crate::kernel::Settings;

#[test]
fn test_parse_price_entry() {
let tests = [
"P 2024-12-30 XAU 2659.64 USD\n",
"P 2024-12-30T20:21:22 XAU 2659.64 USD ; space\n",
"P 2024-12-30 XAU 2659.64 USD; no space\n",
"P 2024-12-30T20:21:22Z XAU 2659.64 USD\n",
"P 2024-12-30T20:21:22+02:00 XAU 2659.64 USD\n",
"P 2024-12-30T20:21:22.12 XAU 2659.64 USD\n",
];

for s in tests {
let mut settings = Settings::default();

let mut is = Stream {
input: s,
state: &mut settings,
};

let res = parse_price_entry(&mut is);

assert!(res.is_ok());
}
}
}
2 changes: 1 addition & 1 deletion tackler-core/src/parser/parts/txns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use winnow::ascii::{line_ending, multispace0, space0};
use winnow::combinator::{cut_err, eof, opt, preceded, repeat, repeat_till};
use winnow::error::StrContext;

fn multispace0_line_ending<'s>(is: &mut Stream<'s>) -> PResult<&'s str> {
pub(crate) fn multispace0_line_ending<'s>(is: &mut Stream<'s>) -> PResult<&'s str> {
// space0 can't be multispace0 as it's greedy and eats away the last line ending
repeat(1.., (space0, line_ending))
.map(|()| ())
Expand Down
108 changes: 108 additions & 0 deletions tackler-core/src/parser/pricedb_parser.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* Copyright 2023-2025 E257.FI and Muhammad Ragib Hasin
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

use winnow::{
combinator::{eof, opt, preceded, repeat_till},
Parser,
};

use crate::kernel::Settings;
use crate::model::price_entry::PriceEntry;
use crate::parser::Stream;

use super::parts::{pricedb::parse_price_entry, txns::multispace0_line_ending};

use std::error::Error;
use std::path::Path;

pub(crate) fn pricedb_from_str(
input: &mut &str,
settings: &mut Settings,
) -> Result<Vec<PriceEntry>, Box<dyn Error>> {
let is = Stream {
input,
state: settings,
};

preceded(
opt(multispace0_line_ending),
repeat_till(1.., parse_price_entry, eof),
)
.parse(is)
.map(|(price_entries, _)| price_entries)
.map_err(|err| err.to_string().into())
// .map_err(|err| {
// let mut msg = "Failed to process txn input\n".to_string();
// //let _ = writeln!(msg, "Error: {}", err);
// match err.into_inner() {
// Some(ce) => {
// if let Some(cause) = ce.cause() {
// let _ = writeln!(msg, "Cause:\n{}\n", cause);
// }
// let _ = writeln!(msg, "Error backtrace:");
// for c in ce.context() {
// let _ = writeln!(msg, " {}", c);
// }
// }
// None => {
// let _ = write!(msg, "No detailed error information available");
// }
// }
// let i = is.input.lines().next().unwrap_or(is.input);
// let i_err = if i.chars().count() < 1024 {
// i.to_string()
// } else {
// i.chars().take(1024).collect::<String>()
// };

// let _ = write!(msg, "Failed input:\n{}\n\n", i_err);

// msg.into()
// })
}

pub(crate) fn pricedb_from_file(
path: &Path,
settings: &mut Settings,
) -> Result<Vec<PriceEntry>, Box<dyn Error>> {
let pricedb_str = std::fs::read_to_string(path)
.map_err(|err| format!("Can't open file: '{}' - {}", path.display(), err))?;

// todo: error log
pricedb_from_str(&mut &*pricedb_str, settings)
}

#[cfg(test)]
mod tests {
use super::*;
use crate::kernel::Settings;

#[test]
fn test_parse_pricedb() {
let test = r#"
P 2024-01-09 XAU 2659.645203 USD
P 2024-01-09 USD 121.306155 BDT
P 2024-01-09 XAG 3652.77663 BDT
"#;

let mut settings = Settings::default();

let res = pricedb_from_str(&mut &*test, &mut settings);

assert!(res.is_ok());
}
}

0 comments on commit 7f0ade5

Please sign in to comment.