Refactoring and documentation

This commit is contained in:
RunasSudo 2025-05-21 19:59:57 +10:00
parent d422e83275
commit 9e34360abf
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
6 changed files with 323 additions and 255 deletions

View File

@ -17,13 +17,13 @@
*/
use chrono::NaiveDate;
use libdrcr::reporting::{
builders::register_dynamic_builders,
calculator::solve_for,
steps::{
register_lookup_fns, AllTransactionsExceptRetainedEarnings,
AllTransactionsIncludingRetainedEarnings, CalculateIncomeTax,
},
use libdrcr::reporting::builders::register_dynamic_builders;
use libdrcr::reporting::calculator::steps_for_targets;
use libdrcr::reporting::steps::{
register_lookup_fns, AllTransactionsExceptRetainedEarnings,
AllTransactionsIncludingRetainedEarnings, CalculateIncomeTax,
};
use libdrcr::reporting::types::{
DateArgs, DateStartDateEndArgs, ReportingContext, ReportingProductKind, ReportingStep,
};
@ -44,7 +44,7 @@ fn main() {
];
println!("For income statement:");
match solve_for(targets, context) {
match steps_for_targets(targets, context) {
Ok(steps) => {
for step in steps {
println!("- {}", step);
@ -67,7 +67,7 @@ fn main() {
];
println!("For balance sheet:");
match solve_for(targets, context) {
match steps_for_targets(targets, context) {
Ok(steps) => {
for step in steps {
println!("- {}", step);

View File

@ -18,8 +18,8 @@
use std::fmt::Display;
use super::{
calculator::{has_step_or_can_build, HasStepOrCanBuild, ReportingGraphDependencies},
use super::calculator::{has_step_or_can_build, HasStepOrCanBuild, ReportingGraphDependencies};
use super::types::{
DateArgs, DateStartDateEndArgs, ReportingContext, ReportingProductId, ReportingProductKind,
ReportingStep, ReportingStepArgs, ReportingStepDynamicBuilder, ReportingStepId,
};

View File

@ -16,50 +16,37 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use super::{
//! This module implements the dependency resolution for [ReportingStep]s
use super::types::{
ReportingContext, ReportingProductId, ReportingStep, ReportingStepDynamicBuilder,
ReportingStepFromArgsFn, ReportingStepId,
};
/// List of dependencies between [ReportingStep]s and [ReportingProduct]s
#[derive(Debug)]
pub struct ReportingGraphDependencies {
vec: Vec<Dependency>,
}
impl ReportingGraphDependencies {
/// Get the list of [Dependency]s
pub fn vec(&self) -> &Vec<Dependency> {
&self.vec
}
pub fn add_dependency(&mut self, step: ReportingStepId, dependency: ReportingProductId) {
/// Record that the [ReportingStep] depends on the [ReportingProduct]
pub fn add_dependency(&mut self, step: ReportingStepId, product: ReportingProductId) {
if !self
.vec
.iter()
.any(|d| d.step == step && d.product == dependency)
.any(|d| d.step == step && d.product == product)
{
self.vec.push(Dependency { step, product: dependency });
self.vec.push(Dependency { step, product });
}
}
/*pub fn add_target_dependency(&mut self, target: ReportingStepId, dependency: ReportingStepId) {
for kind in target.product_kinds {
match kind {
ReportingProductKind::Transactions | ReportingProductKind::BalancesBetween => {
self.add_dependency(
target.clone(),
ReportingProductId {
name: dependency.name,
kind: *kind,
args: target.args.clone(),
},
);
}
ReportingProductKind::BalancesAt => todo!(),
ReportingProductKind::Generic => todo!(),
}
}
}*/
/// Get the [Dependency]s for the given [ReportingStep]
pub fn dependencies_for_step(&self, step: &ReportingStepId) -> Vec<&Dependency> {
return self.vec.iter().filter(|d| d.step == *step).collect();
}
@ -72,6 +59,7 @@ pub struct Dependency {
pub product: ReportingProductId,
}
/// Indicates an error during dependency resolution in [steps_for_targets]
#[derive(Debug)]
pub enum ReportingCalculationError {
UnknownStep { message: String },
@ -86,6 +74,7 @@ pub enum HasStepOrCanBuild<'a, 'b> {
None,
}
/// Determines whether the [ReportingProduct] is generated by a known step, or can be generated by a lookup function or dynamic builder
pub fn has_step_or_can_build<'a, 'b>(
product: &ReportingProductId,
steps: &'a Vec<Box<dyn ReportingStep>>,
@ -129,23 +118,15 @@ pub fn has_step_or_can_build<'a, 'b>(
return HasStepOrCanBuild::None;
}
/// Check whether the [ReportingStep] would be ready to execute, if the given previous steps have already completed
fn would_be_ready_to_execute(
step: &Box<dyn ReportingStep>,
steps: &Vec<Box<dyn ReportingStep>>,
dependencies: &ReportingGraphDependencies,
previous_steps: &Vec<usize>,
) -> bool {
//println!(
// "- would_be_ready_to_execute: {}, {:?}",
// step.id(),
// previous_steps
//);
// Check whether the step would be ready to execute, if the previous steps have already completed
'check_each_dependency: for dependency in dependencies.vec.iter() {
if dependency.step == step.id() {
//println!("-- {}", dependency.dependency);
// Check if the dependency has been produced by a previous step
for previous_step in previous_steps {
if steps[*previous_step].id().name == dependency.product.name
@ -166,7 +147,8 @@ fn would_be_ready_to_execute(
true
}
pub fn solve_for(
/// Recursively resolve the dependencies of the target [ReportingStep]s and return a sorted [Vec] of [ReportingStep]s
pub fn steps_for_targets(
targets: Vec<Box<dyn ReportingStep>>,
context: ReportingContext,
) -> Result<Vec<Box<dyn ReportingStep>>, ReportingCalculationError> {
@ -180,12 +162,15 @@ pub fn solve_for(
for dependency in target.requires(&context) {
dependencies.add_dependency(target.id(), dependency);
}
target.as_ref().init_graph(&steps, &mut dependencies, &context);
target
.as_ref()
.init_graph(&steps, &mut dependencies, &context);
}
// Call after_init_graph on targets
for step in steps.iter() {
step.as_ref().after_init_graph(&steps, &mut dependencies, &context);
step.as_ref()
.after_init_graph(&steps, &mut dependencies, &context);
}
// Process dependencies
@ -204,8 +189,7 @@ pub fn solve_for(
}) {
// Try lookup function
if let Some(lookup_key) = context.step_lookup_fn.keys().find(|(name, kinds)| {
*name == dependency.product.name
&& kinds.contains(&dependency.product.kind)
*name == dependency.product.name && kinds.contains(&dependency.product.kind)
}) {
let (takes_args_fn, from_args_fn) =
context.step_lookup_fn.get(lookup_key).unwrap();
@ -269,12 +253,15 @@ pub fn solve_for(
for dependency in new_step.requires(&context) {
dependencies.add_dependency(new_step.id(), dependency);
}
new_step.as_ref().init_graph(&steps, &mut dependencies, &context);
new_step
.as_ref()
.init_graph(&steps, &mut dependencies, &context);
}
// Call after_init_graph on all steps
for step in steps.iter() {
step.as_ref().after_init_graph(&steps, &mut dependencies, &context);
step.as_ref()
.after_init_graph(&steps, &mut dependencies, &context);
}
}

View File

@ -16,206 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::fmt::Debug;
use std::{collections::HashMap, fmt::Display};
use calculator::ReportingGraphDependencies;
use chrono::NaiveDate;
use downcast_rs::Downcast;
use dyn_clone::DynClone;
use dyn_eq::DynEq;
pub mod builders;
pub mod calculator;
pub mod steps;
pub struct ReportingContext {
eofy_date: NaiveDate,
step_lookup_fn: HashMap<
(&'static str, &'static [ReportingProductKind]),
(ReportingStepTakesArgsFn, ReportingStepFromArgsFn),
>,
step_dynamic_builders: Vec<ReportingStepDynamicBuilder>,
}
impl ReportingContext {
pub fn new(eofy_date: NaiveDate) -> Self {
Self {
eofy_date: eofy_date,
step_lookup_fn: HashMap::new(),
step_dynamic_builders: Vec::new(),
}
}
fn register_lookup_fn(
&mut self,
name: &'static str,
product_kinds: &'static [ReportingProductKind],
takes_args_fn: ReportingStepTakesArgsFn,
from_args_fn: ReportingStepFromArgsFn,
) {
self.step_lookup_fn
.insert((name, product_kinds), (takes_args_fn, from_args_fn));
}
fn register_dynamic_builder(&mut self, builder: ReportingStepDynamicBuilder) {
if !self
.step_dynamic_builders
.iter()
.any(|b| b.name == builder.name)
{
self.step_dynamic_builders.push(builder);
}
}
}
#[derive(Debug, Eq, PartialEq)]
pub struct ReportingProductId {
name: &'static str,
kind: ReportingProductKind,
args: Box<dyn ReportingStepArgs>,
}
impl Display for ReportingProductId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}.{:?}({})", self.name, self.kind, self.args))
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum ReportingProductKind {
Transactions,
BalancesAt,
BalancesBetween,
Generic,
}
//enum ReportingProduct {
// Transactions(Transactions),
// BalancesAt(BalancesAt),
// BalancesBetween(BalancesBetween),
// Generic(Box<dyn GenericReportingProduct>),
//}
//struct Transactions {}
//struct BalancesAt {}
//struct BalancesBetween {}
//trait GenericReportingProduct {}
//type ReportingProducts = HashMap<ReportingProductId, ReportingProduct>;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ReportingStepId {
pub name: &'static str,
pub product_kinds: &'static [ReportingProductKind],
pub args: Box<dyn ReportingStepArgs>,
}
impl Display for ReportingStepId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!(
"{}{:?}({})",
self.name, self.product_kinds, self.args
))
}
}
pub trait ReportingStep: Debug + Display + Downcast {
// Info
fn id(&self) -> ReportingStepId;
// Methods
fn requires(&self, _context: &ReportingContext) -> Vec<ReportingProductId> {
vec![]
}
fn init_graph(
&self,
_steps: &Vec<Box<dyn ReportingStep>>,
_dependencies: &mut ReportingGraphDependencies,
_context: &ReportingContext,
) {
}
fn after_init_graph(
&self,
_steps: &Vec<Box<dyn ReportingStep>>,
_dependencies: &mut ReportingGraphDependencies,
_context: &ReportingContext,
) {
}
//fn execute(&self, _context: &ReportingContext, _products: &mut ReportingProducts) {
// todo!();
//}
}
downcast_rs::impl_downcast!(ReportingStep);
pub trait ReportingStepArgs: Debug + Display + Downcast + DynClone + DynEq {}
downcast_rs::impl_downcast!(ReportingStepArgs);
dyn_clone::clone_trait_object!(ReportingStepArgs);
dyn_eq::eq_trait_object!(ReportingStepArgs);
pub type ReportingStepTakesArgsFn = fn(args: &Box<dyn ReportingStepArgs>) -> bool;
pub type ReportingStepFromArgsFn = fn(args: Box<dyn ReportingStepArgs>) -> Box<dyn ReportingStep>;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VoidArgs {}
impl ReportingStepArgs for VoidArgs {}
impl Display for VoidArgs {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!(""))
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DateArgs {
pub date: NaiveDate,
}
impl ReportingStepArgs for DateArgs {}
impl Display for DateArgs {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}", self.date))
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DateStartDateEndArgs {
pub date_start: NaiveDate,
pub date_end: NaiveDate,
}
impl ReportingStepArgs for DateStartDateEndArgs {}
impl Display for DateStartDateEndArgs {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}, {}", self.date_start, self.date_end))
}
}
pub struct ReportingStepDynamicBuilder {
name: &'static str,
can_build: fn(
name: &'static str,
kind: ReportingProductKind,
args: &Box<dyn ReportingStepArgs>,
steps: &Vec<Box<dyn ReportingStep>>,
dependencies: &ReportingGraphDependencies,
context: &ReportingContext,
) -> bool,
build: fn(
name: &'static str,
kind: ReportingProductKind,
args: Box<dyn ReportingStepArgs>,
steps: &Vec<Box<dyn ReportingStep>>,
dependencies: &ReportingGraphDependencies,
context: &ReportingContext,
) -> Box<dyn ReportingStep>,
}
pub mod types;

View File

@ -20,13 +20,11 @@ use std::fmt::Display;
use chrono::Datelike;
use crate::reporting::types::{BalancesAt, DateStartDateEndArgs, ReportingProduct, ReportingProductId};
use crate::util::sofy_from_eofy;
use super::{
calculator::ReportingGraphDependencies, DateArgs, DateStartDateEndArgs, ReportingContext,
ReportingProductId, ReportingProductKind, ReportingStep, ReportingStepArgs, ReportingStepId,
VoidArgs,
};
use super:: calculator::ReportingGraphDependencies;
use super::types::{DateArgs, ReportingContext, ReportingProductKind, ReportingProducts, ReportingStep, ReportingStepArgs, ReportingStepId, VoidArgs};
pub fn register_lookup_fns(context: &mut ReportingContext) {
context.register_lookup_fn(

282
src/reporting/types.rs Normal file
View File

@ -0,0 +1,282 @@
/*
DrCr: Web-based double-entry bookkeeping framework
Copyright (C) 2022-2025 Lee Yingtong Li (RunasSudo)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::collections::HashMap;
use std::fmt::{Debug, Display};
use chrono::NaiveDate;
use downcast_rs::Downcast;
use dyn_clone::DynClone;
use dyn_eq::DynEq;
use dyn_hash::DynHash;
use crate::QuantityInt;
use super::calculator::ReportingGraphDependencies;
// -----------------
// REPORTING CONTEXT
/// Records the context for a single reporting job
pub struct ReportingContext {
pub eofy_date: NaiveDate,
pub(crate) step_lookup_fn: HashMap<
(&'static str, &'static [ReportingProductKind]),
(ReportingStepTakesArgsFn, ReportingStepFromArgsFn),
>,
pub(crate) step_dynamic_builders: Vec<ReportingStepDynamicBuilder>,
}
impl ReportingContext {
/// Initialise a new [ReportingContext]
pub fn new(eofy_date: NaiveDate) -> Self {
Self {
eofy_date: eofy_date,
step_lookup_fn: HashMap::new(),
step_dynamic_builders: Vec::new(),
}
}
/// Register a lookup function
///
/// A lookup function generates concrete [ReportingStep]s from a [ReportingStepId].
pub fn register_lookup_fn(
&mut self,
name: &'static str,
product_kinds: &'static [ReportingProductKind],
takes_args_fn: ReportingStepTakesArgsFn,
from_args_fn: ReportingStepFromArgsFn,
) {
self.step_lookup_fn
.insert((name, product_kinds), (takes_args_fn, from_args_fn));
}
/// Register a dynamic builder
///
/// Dynamic builders are called when no concrete [ReportingStep] is implemented, and can dynamically generate a [ReportingStep]. Dynamic builders are implemented in [super::builders].
pub fn register_dynamic_builder(&mut self, builder: ReportingStepDynamicBuilder) {
if !self
.step_dynamic_builders
.iter()
.any(|b| b.name == builder.name)
{
self.step_dynamic_builders.push(builder);
}
}
}
/// Function which determines whether the [ReportingStepArgs] are valid arguments for a given [ReportingStep]
///
/// See [ReportingContext::register_lookup_fn].
pub type ReportingStepTakesArgsFn = fn(args: &Box<dyn ReportingStepArgs>) -> bool;
/// Function which builds a concrete [ReportingStep] from the given [ReportingStepArgs]
///
/// See [ReportingContext::register_lookup_fn].
pub type ReportingStepFromArgsFn = fn(args: Box<dyn ReportingStepArgs>) -> Box<dyn ReportingStep>;
// -------------------------------
// REPORTING STEP DYNAMIC BUILDERS
/// Represents a reporting step dynamic builder
///
/// See [ReportingContext::register_dynamic_builder].
pub struct ReportingStepDynamicBuilder {
pub name: &'static str,
pub can_build: fn(
name: &'static str,
kind: ReportingProductKind,
args: &Box<dyn ReportingStepArgs>,
steps: &Vec<Box<dyn ReportingStep>>,
dependencies: &ReportingGraphDependencies,
context: &ReportingContext,
) -> bool,
pub build: fn(
name: &'static str,
kind: ReportingProductKind,
args: Box<dyn ReportingStepArgs>,
steps: &Vec<Box<dyn ReportingStep>>,
dependencies: &ReportingGraphDependencies,
context: &ReportingContext,
) -> Box<dyn ReportingStep>,
}
// ------------------
// REPORTING PRODUCTS
/// Identifies a [ReportingProduct]
#[derive(Debug, Eq, Hash, PartialEq)]
pub struct ReportingProductId {
pub name: &'static str,
pub kind: ReportingProductKind,
pub args: Box<dyn ReportingStepArgs>,
}
impl Display for ReportingProductId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}.{:?}({})", self.name, self.kind, self.args))
}
}
/// Identifies a type of [ReportingProduct]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum ReportingProductKind {
Transactions,
BalancesAt,
BalancesBetween,
Generic,
}
/// Represents the result of a [ReportingStep]
pub enum ReportingProduct {
Transactions(Transactions),
BalancesAt(BalancesAt),
BalancesBetween(BalancesBetween),
Generic(Box<dyn GenericReportingProduct>),
}
/// Records a list of transactions generated by a [ReportingStep]
pub struct Transactions {}
/// Records cumulative account balances at a particular point in time
pub struct BalancesAt {
pub balances: HashMap<String, QuantityInt>,
}
/// Records the total value of transactions in each account between two points in time
pub struct BalancesBetween {}
/// Represents a custom [ReportingProduct] generated by a [ReportingStep]
pub trait GenericReportingProduct {}
/// Convenience type mapping [ReportingProductId] to [ReportingProduct]
pub type ReportingProducts = HashMap<ReportingProductId, ReportingProduct>;
// ---------------
// REPORTING STEPS
/// Identifies a [ReportingStep]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ReportingStepId {
pub name: &'static str,
pub product_kinds: &'static [ReportingProductKind],
pub args: Box<dyn ReportingStepArgs>,
}
impl Display for ReportingStepId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!(
"{}{:?}({})",
self.name, self.product_kinds, self.args
))
}
}
/// Represents a step in a reporting job
pub trait ReportingStep: Debug + Display + Downcast {
/// Get the [ReportingStepId] for this [ReportingStep]
fn id(&self) -> ReportingStepId;
/// Return a list of statically defined dependencies for this [ReportingStep]
#[allow(unused_variables)]
fn requires(&self, context: &ReportingContext) -> Vec<ReportingProductId> {
vec![]
}
/// Called when the [ReportingStep] is initialised in [super::calculator::steps_for_targets]
#[allow(unused_variables)]
fn init_graph(
&self,
steps: &Vec<Box<dyn ReportingStep>>,
dependencies: &mut ReportingGraphDependencies,
context: &ReportingContext,
) {
}
/// Called when new [ReportingStep]s are initialised in [super::calculator::steps_for_targets]
///
/// This callback can be used to dynamically declare dependencies between [ReportingStep]s that are not known at initialisation.
#[allow(unused_variables)]
fn after_init_graph(
&self,
steps: &Vec<Box<dyn ReportingStep>>,
dependencies: &mut ReportingGraphDependencies,
context: &ReportingContext,
) {
}
/// Called to generate the [ReportingProduct] for this [ReportingStep]
#[allow(unused_variables)]
fn execute(&self, context: &ReportingContext, products: &mut ReportingProducts) {
todo!();
}
}
downcast_rs::impl_downcast!(ReportingStep);
// ------------------------
// REPORTING STEP ARGUMENTS
/// Represents arguments to a [ReportingStep]
pub trait ReportingStepArgs: Debug + Display + Downcast + DynClone + DynEq + DynHash {}
downcast_rs::impl_downcast!(ReportingStepArgs);
dyn_clone::clone_trait_object!(ReportingStepArgs);
dyn_eq::eq_trait_object!(ReportingStepArgs);
dyn_hash::hash_trait_object!(ReportingStepArgs);
/// [ReportingStepArgs] implementation which takes no arguments
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct VoidArgs {}
impl ReportingStepArgs for VoidArgs {}
impl Display for VoidArgs {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!(""))
}
}
/// [ReportingStepArgs] implementation which takes a single date
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct DateArgs {
pub date: NaiveDate,
}
impl ReportingStepArgs for DateArgs {}
impl Display for DateArgs {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}", self.date))
}
}
/// [ReportingStepArgs] implementation which takes a date range
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct DateStartDateEndArgs {
pub date_start: NaiveDate,
pub date_end: NaiveDate,
}
impl ReportingStepArgs for DateStartDateEndArgs {}
impl Display for DateStartDateEndArgs {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}, {}", self.date_start, self.date_end))
}
}