diff --git a/src/main.rs b/src/main.rs index 30bd029..02ea9f7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,8 +20,12 @@ use chrono::NaiveDate; use libdrcr::reporting::{ builders::register_dynamic_builders, calculator::solve_for, - steps::{register_lookup_fns, AllTransactionsExceptRetainedEarnings, CalculateIncomeTax}, - DateEofyArgs, DateStartDateEndArgs, ReportingContext, ReportingStep, + steps::{ + register_lookup_fns, AllTransactionsExceptRetainedEarnings, + AllTransactionsIncludingRetainedEarnings, CalculateIncomeTax, + }, + DateArgs, DateEofyArgs, DateStartDateEndArgs, ReportingContext, ReportingProductKind, + ReportingStep, }; fn main() { @@ -36,13 +40,43 @@ fn main() { }, }), Box::new(AllTransactionsExceptRetainedEarnings { - args: DateStartDateEndArgs { + product_kinds: &[ReportingProductKind::BalancesBetween], + args: Box::new(DateStartDateEndArgs { date_start: NaiveDate::from_ymd_opt(2024, 7, 1).unwrap(), date_end: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), - }, + }), }), ]; + println!("For income statement:"); + match solve_for(targets, context) { + Ok(steps) => { + for step in steps { + println!("- {}", step); + } + } + Err(err) => panic!("Error: {:?}", err), + } + + let mut context = ReportingContext::new(NaiveDate::from_ymd_opt(2025, 6, 30).unwrap()); + register_lookup_fns(&mut context); + register_dynamic_builders(&mut context); + + let targets: Vec> = vec![ + Box::new(CalculateIncomeTax { + args: DateEofyArgs { + date_eofy: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), + }, + }), + Box::new(AllTransactionsIncludingRetainedEarnings { + product_kinds: &[ReportingProductKind::BalancesAt], + args: Box::new(DateArgs { + date: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), + }), + }), + ]; + + println!("For balance sheet:"); match solve_for(targets, context) { Ok(steps) => { for step in steps { diff --git a/src/reporting/builders.rs b/src/reporting/builders.rs index 7905298..369df9c 100644 --- a/src/reporting/builders.rs +++ b/src/reporting/builders.rs @@ -25,12 +25,6 @@ use super::{ }; pub fn register_dynamic_builders(context: &mut ReportingContext) { - context.register_dynamic_builder(ReportingStepDynamicBuilder { - name: "BalancesAtToBalancesBetween", - can_build: BalancesAtToBalancesBetween::can_build, - build: BalancesAtToBalancesBetween::build, - }); - context.register_dynamic_builder(ReportingStepDynamicBuilder { name: "GenerateBalances", can_build: GenerateBalances::can_build, @@ -42,6 +36,19 @@ pub fn register_dynamic_builders(context: &mut ReportingContext) { can_build: UpdateBalancesBetween::can_build, build: UpdateBalancesBetween::build, }); + + context.register_dynamic_builder(ReportingStepDynamicBuilder { + name: "UpdateBalancesAt", + can_build: UpdateBalancesAt::can_build, + build: UpdateBalancesAt::build, + }); + + // This is the least efficient way of generating BalancesBetween + context.register_dynamic_builder(ReportingStepDynamicBuilder { + name: "BalancesAtToBalancesBetween", + can_build: BalancesAtToBalancesBetween::can_build, + build: BalancesAtToBalancesBetween::build, + }); } #[derive(Debug)] @@ -63,6 +70,10 @@ impl BalancesAtToBalancesBetween { ) -> bool { // Check for BalancesAt, BalancesAt -> BalancesBetween if kind == ReportingProductKind::BalancesBetween { + if !args.is::() { + return false; + } + let args = args.downcast_ref::().unwrap(); match has_step_or_can_build( @@ -105,7 +116,10 @@ impl BalancesAtToBalancesBetween { impl Display for BalancesAtToBalancesBetween { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("{} {{BalancesAtToBalancesBetween}}", self.id())) + f.write_fmt(format_args!( + "{} {{BalancesAtToBalancesBetween}}", + self.id() + )) } } @@ -227,6 +241,133 @@ impl ReportingStep for GenerateBalances { } } +#[derive(Debug)] +pub struct UpdateBalancesAt { + step_name: &'static str, + args: DateArgs, +} + +impl UpdateBalancesAt { + // Implements (BalancesAt -> Transactions) -> BalancesAt + + fn can_build( + name: &'static str, + kind: ReportingProductKind, + _args: &Box, + steps: &Vec>, + dependencies: &ReportingGraphDependencies, + context: &ReportingContext, + ) -> bool { + // Check for Transactions -> BalancesAt + if kind == ReportingProductKind::BalancesAt { + // Initially no need to check args + if let Some(step) = steps.iter().find(|s| { + s.id().name == name + && s.id() + .product_kinds + .contains(&ReportingProductKind::Transactions) + }) { + // Check for BalancesAt -> Transactions + let dependencies_for_step = dependencies.dependencies_for_step(&step.id()); + if dependencies_for_step.len() == 1 + && dependencies_for_step[0].dependency.kind == ReportingProductKind::BalancesAt + { + return true; + } + + // Check if BalancesBetween -> Transactions and BalancesAt is available + if dependencies_for_step.len() == 1 + && dependencies_for_step[0].dependency.kind + == ReportingProductKind::BalancesBetween + { + let date_end = dependencies_for_step[0] + .dependency + .args + .downcast_ref::() + .unwrap() + .date_end; + + match has_step_or_can_build( + &ReportingProductId { + name: dependencies_for_step[0].dependency.name, + kind: ReportingProductKind::BalancesAt, + args: Box::new(DateArgs { date: date_end }), + }, + steps, + dependencies, + context, + ) { + HasStepOrCanBuild::HasStep(_) + | HasStepOrCanBuild::CanLookup(_) + | HasStepOrCanBuild::CanBuild(_) => { + return true; + } + HasStepOrCanBuild::None => {} + } + } + } + } + return false; + } + + fn build( + name: &'static str, + _kind: ReportingProductKind, + args: Box, + _steps: &Vec>, + _dependencies: &ReportingGraphDependencies, + _context: &ReportingContext, + ) -> Box { + Box::new(UpdateBalancesAt { + step_name: name, + args: *args.downcast().unwrap(), + }) + } +} + +impl Display for UpdateBalancesAt { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{} {{UpdateBalancesAt}}", self.id())) + } +} + +impl ReportingStep for UpdateBalancesAt { + fn id(&self) -> ReportingStepId { + ReportingStepId { + name: self.step_name, + product_kinds: &[ReportingProductKind::BalancesAt], + args: Box::new(self.args.clone()), + } + } + + fn init_graph( + &self, + steps: &Vec>, + dependencies: &mut ReportingGraphDependencies, + ) { + // Add a dependency on the Transactions result + // Look up that step, so we can extract the appropriate args + let parent_step = steps + .iter() + .find(|s| { + s.id().name == self.step_name + && s.id() + .product_kinds + .contains(&ReportingProductKind::Transactions) + }) + .unwrap(); // Existence is checked in can_build + + dependencies.add_dependency( + self.id(), + ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::Transactions, + args: parent_step.id().args.clone(), + }, + ); + } +} + #[derive(Debug)] pub struct UpdateBalancesBetween { step_name: &'static str, diff --git a/src/reporting/calculator.rs b/src/reporting/calculator.rs index e5c2771..2073a43 100644 --- a/src/reporting/calculator.rs +++ b/src/reporting/calculator.rs @@ -17,8 +17,8 @@ */ use super::{ - ReportingContext, ReportingProductId, ReportingProductKind, ReportingStep, - ReportingStepDynamicBuilder, ReportingStepFromArgsFn, ReportingStepId, + ReportingContext, ReportingProductId, ReportingStep, ReportingStepDynamicBuilder, + ReportingStepFromArgsFn, ReportingStepId, }; #[derive(Debug)] @@ -41,7 +41,7 @@ impl ReportingGraphDependencies { } } - pub fn add_target_dependency(&mut self, target: ReportingStepId, dependency: ReportingStepId) { + /*pub fn add_target_dependency(&mut self, target: ReportingStepId, dependency: ReportingStepId) { for kind in target.product_kinds { match kind { ReportingProductKind::Transactions | ReportingProductKind::BalancesBetween => { @@ -58,7 +58,7 @@ impl ReportingGraphDependencies { ReportingProductKind::Generic => todo!(), } } - } + }*/ pub fn dependencies_for_step(&self, step: &ReportingStepId) -> Vec<&Dependency> { return self.vec.iter().filter(|d| d.step == *step).collect(); @@ -206,46 +206,50 @@ pub fn solve_for( *name == dependency.dependency.name && kinds.contains(&dependency.dependency.kind) }) { - let (_, lookup_fn) = context.step_lookup_fn.get(lookup_key).unwrap(); - let new_step = lookup_fn(dependency.dependency.args.clone()); + let (takes_args_fn, from_args_fn) = + context.step_lookup_fn.get(lookup_key).unwrap(); + if takes_args_fn(&dependency.dependency.args) { + let new_step = from_args_fn(dependency.dependency.args.clone()); - // Check new step meets the dependency - if new_step.id().name != dependency.dependency.name { - panic!("Unexpected step returned from lookup function (expected name {}, got {})", dependency.dependency.name, new_step.id().name); - } - if new_step.id().args != dependency.dependency.args { - panic!("Unexpected step returned from lookup function {} (expected args {:?}, got {:?})", dependency.dependency.name, dependency.dependency.args, new_step.id().args); - } - if !new_step - .id() - .product_kinds - .contains(&dependency.dependency.kind) - { - panic!("Unexpected step returned from lookup function {} (expected kind {:?}, got {:?})", dependency.dependency.name, dependency.dependency.kind, new_step.id().product_kinds); - } + // Check new step meets the dependency + if new_step.id().name != dependency.dependency.name { + panic!("Unexpected step returned from lookup function (expected name {}, got {})", dependency.dependency.name, new_step.id().name); + } + if new_step.id().args != dependency.dependency.args { + panic!("Unexpected step returned from lookup function {} (expected args {:?}, got {:?})", dependency.dependency.name, dependency.dependency.args, new_step.id().args); + } + if !new_step + .id() + .product_kinds + .contains(&dependency.dependency.kind) + { + panic!("Unexpected step returned from lookup function {} (expected kind {:?}, got {:?})", dependency.dependency.name, dependency.dependency.kind, new_step.id().product_kinds); + } - new_steps.push(new_step); - } else { - // No explicit step for product - try builders - for builder in context.step_dynamic_builders.iter() { - if (builder.can_build)( + new_steps.push(new_step); + continue; + } + } + + // No explicit step for product - try builders + for builder in context.step_dynamic_builders.iter() { + if (builder.can_build)( + dependency.dependency.name, + dependency.dependency.kind, + &dependency.dependency.args, + &steps, + &dependencies, + &context, + ) { + new_steps.push((builder.build)( dependency.dependency.name, dependency.dependency.kind, - &dependency.dependency.args, + dependency.dependency.args.clone(), &steps, &dependencies, &context, - ) { - new_steps.push((builder.build)( - dependency.dependency.name, - dependency.dependency.kind, - dependency.dependency.args.clone(), - &steps, - &dependencies, - &context, - )); - break; - } + )); + break; } } } @@ -267,9 +271,9 @@ pub fn solve_for( new_step.as_ref().init_graph(&steps, &mut dependencies); } - // Call after_init_graph on new steps - for new_step_index in new_step_indexes { - steps[new_step_index].after_init_graph(&steps, &mut dependencies); + // Call after_init_graph on all steps + for step in steps.iter() { + step.as_ref().after_init_graph(&steps, &mut dependencies); } } diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index c3f8f19..bf512ed 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -27,11 +27,49 @@ use super::{ }; pub fn register_lookup_fns(context: &mut ReportingContext) { + context.register_lookup_fn( + "AllTransactionsExceptRetainedEarnings", + &[ReportingProductKind::BalancesAt], + AllTransactionsExceptRetainedEarnings::takes_args, + |a| { + AllTransactionsExceptRetainedEarnings::from_args(&[ReportingProductKind::BalancesAt], a) + }, + ); + context.register_lookup_fn( "AllTransactionsExceptRetainedEarnings", &[ReportingProductKind::BalancesBetween], AllTransactionsExceptRetainedEarnings::takes_args, - AllTransactionsExceptRetainedEarnings::from_args, + |a| { + AllTransactionsExceptRetainedEarnings::from_args( + &[ReportingProductKind::BalancesBetween], + a, + ) + }, + ); + + context.register_lookup_fn( + "AllTransactionsIncludingRetainedEarnings", + &[ReportingProductKind::BalancesAt], + AllTransactionsIncludingRetainedEarnings::takes_args, + |a| { + AllTransactionsIncludingRetainedEarnings::from_args( + &[ReportingProductKind::BalancesAt], + a, + ) + }, + ); + + context.register_lookup_fn( + "AllTransactionsIncludingRetainedEarnings", + &[ReportingProductKind::BalancesBetween], + AllTransactionsIncludingRetainedEarnings::takes_args, + |a| { + AllTransactionsIncludingRetainedEarnings::from_args( + &[ReportingProductKind::BalancesBetween], + a, + ) + }, ); context.register_lookup_fn( @@ -65,17 +103,22 @@ pub fn register_lookup_fns(context: &mut ReportingContext) { #[derive(Debug)] pub struct AllTransactionsExceptRetainedEarnings { - pub args: DateStartDateEndArgs, + pub product_kinds: &'static [ReportingProductKind], // Must have single member + pub args: Box, } impl AllTransactionsExceptRetainedEarnings { - fn takes_args(args: &Box) -> bool { - args.is::() + fn takes_args(_args: &Box) -> bool { + true } - - fn from_args(args: Box) -> Box { + + fn from_args( + product_kinds: &'static [ReportingProductKind], + args: Box, + ) -> Box { Box::new(AllTransactionsExceptRetainedEarnings { - args: *args.downcast().unwrap(), + product_kinds, + args, }) } } @@ -90,12 +133,58 @@ impl ReportingStep for AllTransactionsExceptRetainedEarnings { fn id(&self) -> ReportingStepId { ReportingStepId { name: "AllTransactionsExceptRetainedEarnings", - product_kinds: &[ReportingProductKind::BalancesBetween], - args: Box::new(self.args.clone()), + product_kinds: self.product_kinds, + args: self.args.clone(), } } } +#[derive(Debug)] +pub struct AllTransactionsIncludingRetainedEarnings { + pub product_kinds: &'static [ReportingProductKind], // Must have single member + pub args: Box, +} + +impl AllTransactionsIncludingRetainedEarnings { + fn takes_args(_args: &Box) -> bool { + true + } + + fn from_args( + product_kinds: &'static [ReportingProductKind], + args: Box, + ) -> Box { + Box::new(AllTransactionsIncludingRetainedEarnings { + product_kinds, + args, + }) + } +} + +impl Display for AllTransactionsIncludingRetainedEarnings { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}", self.id())) + } +} + +impl ReportingStep for AllTransactionsIncludingRetainedEarnings { + fn id(&self) -> ReportingStepId { + ReportingStepId { + name: "AllTransactionsIncludingRetainedEarnings", + product_kinds: self.product_kinds, + args: self.args.clone(), + } + } + + fn requires(&self) -> Vec { + vec![ReportingProductId { + name: "AllTransactionsExceptRetainedEarnings", + kind: self.product_kinds[0], + args: self.args.clone(), + }] + } +} + #[derive(Debug)] pub struct CalculateIncomeTax { pub args: DateEofyArgs, @@ -105,7 +194,7 @@ impl CalculateIncomeTax { fn takes_args(args: &Box) -> bool { args.is::() } - + fn from_args(args: Box) -> Box { Box::new(CalculateIncomeTax { args: *args.downcast().unwrap(), @@ -147,11 +236,35 @@ impl ReportingStep for CalculateIncomeTax { ) { for other in steps { if let Some(other) = other.downcast_ref::() { - if other.args.date_start <= self.args.date_eofy - && other.args.date_end >= self.args.date_eofy - { - // AllTransactionsExceptRetainedEarnings (in applicable periods) depends on CalculateIncomeTax - dependencies.add_target_dependency(other.id(), self.id()); + // AllTransactionsExceptRetainedEarnings (in applicable periods) depends on CalculateIncomeTax + if other.args.is::() { + let other_args = other.args.downcast_ref::().unwrap(); + if other_args.date >= self.args.date_eofy { + dependencies.add_dependency( + other.id(), + ReportingProductId { + name: self.id().name, + kind: other.product_kinds[0], + args: other.id().args, + }, + ); + } + } else if other.args.is::() { + let other_args = other.args.downcast_ref::().unwrap(); + if other_args.date_start <= self.args.date_eofy + && other_args.date_end >= self.args.date_eofy + { + dependencies.add_dependency( + other.id(), + ReportingProductId { + name: self.id().name, + kind: other.product_kinds[0], + args: other.id().args, + }, + ); + } + } else { + unreachable!(); } } } @@ -167,7 +280,7 @@ impl CombineOrdinaryTransactions { fn takes_args(args: &Box) -> bool { args.is::() } - + fn from_args(args: Box) -> Box { Box::new(CombineOrdinaryTransactions { args: *args.downcast().unwrap(), @@ -217,7 +330,7 @@ impl DBBalances { fn takes_args(args: &Box) -> bool { args.is::() } - + fn from_args(args: Box) -> Box { Box::new(DBBalances { args: *args.downcast().unwrap(), @@ -250,7 +363,7 @@ impl PostUnreconciledStatementLines { fn takes_args(args: &Box) -> bool { args.is::() } - + fn from_args(args: Box) -> Box { Box::new(PostUnreconciledStatementLines { args: *args.downcast().unwrap(),