Implement UpdateBalancesAt

This commit is contained in:
RunasSudo 2025-05-21 17:58:42 +10:00
parent b28c75c00f
commit d78d6ed1fe
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
4 changed files with 361 additions and 69 deletions

View File

@ -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<Box<dyn ReportingStep>> = 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 {

View File

@ -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::<DateStartDateEndArgs>() {
return false;
}
let args = args.downcast_ref::<DateStartDateEndArgs>().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<dyn ReportingStepArgs>,
steps: &Vec<Box<dyn ReportingStep>>,
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::<DateStartDateEndArgs>()
.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<dyn ReportingStepArgs>,
_steps: &Vec<Box<dyn ReportingStep>>,
_dependencies: &ReportingGraphDependencies,
_context: &ReportingContext,
) -> Box<dyn ReportingStep> {
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<Box<dyn ReportingStep>>,
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,

View File

@ -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);
}
}

View File

@ -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<dyn ReportingStepArgs>,
}
impl AllTransactionsExceptRetainedEarnings {
fn takes_args(args: &Box<dyn ReportingStepArgs>) -> bool {
args.is::<DateStartDateEndArgs>()
fn takes_args(_args: &Box<dyn ReportingStepArgs>) -> bool {
true
}
fn from_args(args: Box<dyn ReportingStepArgs>) -> Box<dyn ReportingStep> {
fn from_args(
product_kinds: &'static [ReportingProductKind],
args: Box<dyn ReportingStepArgs>,
) -> Box<dyn ReportingStep> {
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<dyn ReportingStepArgs>,
}
impl AllTransactionsIncludingRetainedEarnings {
fn takes_args(_args: &Box<dyn ReportingStepArgs>) -> bool {
true
}
fn from_args(
product_kinds: &'static [ReportingProductKind],
args: Box<dyn ReportingStepArgs>,
) -> Box<dyn ReportingStep> {
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<ReportingProductId> {
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<dyn ReportingStepArgs>) -> bool {
args.is::<DateEofyArgs>()
}
fn from_args(args: Box<dyn ReportingStepArgs>) -> Box<dyn ReportingStep> {
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::<AllTransactionsExceptRetainedEarnings>() {
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::<DateArgs>() {
let other_args = other.args.downcast_ref::<DateArgs>().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::<DateStartDateEndArgs>() {
let other_args = other.args.downcast_ref::<DateStartDateEndArgs>().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<dyn ReportingStepArgs>) -> bool {
args.is::<DateArgs>()
}
fn from_args(args: Box<dyn ReportingStepArgs>) -> Box<dyn ReportingStep> {
Box::new(CombineOrdinaryTransactions {
args: *args.downcast().unwrap(),
@ -217,7 +330,7 @@ impl DBBalances {
fn takes_args(args: &Box<dyn ReportingStepArgs>) -> bool {
args.is::<DateArgs>()
}
fn from_args(args: Box<dyn ReportingStepArgs>) -> Box<dyn ReportingStep> {
Box::new(DBBalances {
args: *args.downcast().unwrap(),
@ -250,7 +363,7 @@ impl PostUnreconciledStatementLines {
fn takes_args(args: &Box<dyn ReportingStepArgs>) -> bool {
args.is::<DateArgs>()
}
fn from_args(args: Box<dyn ReportingStepArgs>) -> Box<dyn ReportingStep> {
Box::new(PostUnreconciledStatementLines {
args: *args.downcast().unwrap(),