From f1d18ec9893bcd6717730a4cdef2aa0376950993 Mon Sep 17 00:00:00 2001 From: aki Date: Fri, 9 May 2025 10:22:35 +0800 Subject: [PATCH] feat: Add the Python project --- .python-version | 1 + building_blocks/financial_concepts.json | 489 ++++++++++++++++++++++++ data/names.json | 69 ++++ data/text_snippets.json | 127 ++++++ data/value_ranges.json | 137 +++++++ main.py | 6 + pyproject.toml | 7 + src/data_loader.py | 63 +++ src/date_utils.py | 133 +++++++ src/formula_evaluator.py | 123 ++++++ src/narrative_builder.py | 209 ++++++++++ src/problem_engine.py | 279 ++++++++++++++ src/solution_presenter.py | 377 ++++++++++++++++++ src/value_sampler.py | 159 ++++++++ 14 files changed, 2179 insertions(+) create mode 100644 .python-version create mode 100644 building_blocks/financial_concepts.json create mode 100644 data/names.json create mode 100644 data/text_snippets.json create mode 100644 data/value_ranges.json create mode 100644 main.py create mode 100644 pyproject.toml create mode 100644 src/data_loader.py create mode 100644 src/date_utils.py create mode 100644 src/formula_evaluator.py create mode 100644 src/narrative_builder.py create mode 100644 src/problem_engine.py create mode 100644 src/solution_presenter.py create mode 100644 src/value_sampler.py diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..bd28b9c --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.9 diff --git a/building_blocks/financial_concepts.json b/building_blocks/financial_concepts.json new file mode 100644 index 0000000..f7b1c85 --- /dev/null +++ b/building_blocks/financial_concepts.json @@ -0,0 +1,489 @@ +[ + { + "concept_id": "SIMPLE_INTEREST_SOLVE_FOR_F", + "description": "Calculates the Future Value (F_simple) in a simple interest problem, given Principal, annual simple interest rate, and time in years.", + "financial_topic": "Simple Interest", + "target_unknown": "F_simple", + "variables_involved": ["P", "i_simple_annual", "n_time_years", "F_simple"], + "formulas": { + "F_simple": "P * (1 + i_simple_annual * n_time_years)" + }, + "required_knowns_for_target": ["P", "i_simple_annual", "n_time_years"], + "narrative_hooks": ["loan", "investment", "borrow", "deposit", "future amount", "accumulated value", "simple interest", "maturity value"], + "solution_step_keys": [ + "solution_guidance.identify_knowns", + "solution_guidance.convert_time_to_years", + "solution_guidance.state_formula", + "solution_guidance.substitute_values", + "solution_guidance.perform_calculation", + "solution_guidance.final_answer_is" + ] + }, + { + "concept_id": "SIMPLE_INTEREST_SOLVE_FOR_P_FROM_F", + "description": "Calculates the Principal (P) in a simple interest problem, given Future Value, annual simple interest rate, and time in years.", + "financial_topic": "Simple Interest", + "target_unknown": "P", + "variables_involved": ["P", "i_simple_annual", "n_time_years", "F_simple"], + "formulas": { + "P": "F_simple / (1 + i_simple_annual * n_time_years)" + }, + "required_knowns_for_target": ["F_simple", "i_simple_annual", "n_time_years"], + "narrative_hooks": ["loan", "investment", "present worth", "initial amount", "deposit now", "simple interest"], + "solution_step_keys": [ + "solution_guidance.identify_knowns", + "solution_guidance.convert_time_to_years", + "solution_guidance.state_formula", + "solution_guidance.substitute_values", + "solution_guidance.perform_calculation", + "solution_guidance.final_answer_is" + ] + }, + { + "concept_id": "SIMPLE_INTEREST_SOLVE_FOR_I", + "description": "Calculates the simple Interest amount (I_simple), given Principal, annual simple interest rate, and time in years.", + "financial_topic": "Simple Interest", + "target_unknown": "I_simple", + "variables_involved": ["P", "i_simple_annual", "n_time_years", "I_simple"], + "formulas": { + "I_simple": "P * i_simple_annual * n_time_years" + }, + "required_knowns_for_target": ["P", "i_simple_annual", "n_time_years"], + "narrative_hooks": ["interest earned", "interest due", "cost of borrowing", "simple interest"], + "solution_step_keys": [ + "solution_guidance.identify_knowns", + "solution_guidance.convert_time_to_years", + "solution_guidance.state_formula", + "solution_guidance.substitute_values", + "solution_guidance.perform_calculation", + "solution_guidance.final_answer_is" + ] + }, + { + "concept_id": "SIMPLE_INTEREST_SOLVE_FOR_P_FROM_I", + "description": "Calculates the Principal (P) in a simple interest problem, given Interest amount, annual simple interest rate, and time in years.", + "financial_topic": "Simple Interest", + "target_unknown": "P", + "variables_involved": ["P", "i_simple_annual", "n_time_years", "I_simple"], + "formulas": { + "P": "I_simple / (i_simple_annual * n_time_years)" + }, + "required_knowns_for_target": ["I_simple", "i_simple_annual", "n_time_years"], + "narrative_hooks": ["principal amount", "original loan", "initial investment", "simple interest", "interest yielded"], + "solution_step_keys": [ + "solution_guidance.identify_knowns", + "solution_guidance.convert_time_to_years", + "solution_guidance.state_formula", + "solution_guidance.substitute_values", + "solution_guidance.perform_calculation", + "solution_guidance.final_answer_is" + ] + }, + { + "concept_id": "SIMPLE_INTEREST_SOLVE_FOR_RATE_FROM_I", + "description": "Calculates the annual simple interest rate (i_simple_annual), given Principal, Interest amount, and time in years.", + "financial_topic": "Simple Interest", + "target_unknown": "i_simple_annual", + "variables_involved": ["P", "i_simple_annual", "n_time_years", "I_simple"], + "formulas": { + "i_simple_annual": "I_simple / (P * n_time_years)" + }, + "required_knowns_for_target": ["P", "I_simple", "n_time_years"], + "narrative_hooks": ["interest rate", "rate of return", "annual rate", "simple interest"], + "solution_step_keys": [ + "solution_guidance.identify_knowns", + "solution_guidance.convert_time_to_years", + "solution_guidance.state_formula", + "solution_guidance.substitute_values", + "solution_guidance.perform_calculation", + "solution_guidance.final_answer_is" + ] + }, + { + "concept_id": "SIMPLE_INTEREST_SOLVE_FOR_TIME_FROM_I", + "description": "Calculates the time period in years (n_time_years), given Principal, Interest amount, and annual simple interest rate.", + "financial_topic": "Simple Interest", + "target_unknown": "n_time_years", + "variables_involved": ["P", "i_simple_annual", "n_time_years", "I_simple"], + "formulas": { + "n_time_years": "I_simple / (P * i_simple_annual)" + }, + "required_knowns_for_target": ["P", "I_simple", "i_simple_annual"], + "narrative_hooks": ["time period", "duration", "loan term", "investment horizon", "simple interest"], + "solution_step_keys": [ + "solution_guidance.identify_knowns", + "solution_guidance.state_formula", + "solution_guidance.substitute_values", + "solution_guidance.perform_calculation", + "solution_guidance.final_answer_is" + ] + }, + { + "concept_id": "COMPOUND_INTEREST_SOLVE_FOR_F", + "description": "Calculates the Future Value (F_compound) in a compound interest problem, given Principal, nominal annual interest rate, compounding frequency, and time in years.", + "financial_topic": "Compound Interest", + "target_unknown": "F_compound", + "variables_involved": ["P", "r_nominal_annual", "m_compounding_periods_per_year", "t_years", "i_rate_per_period", "n_total_compounding_periods", "F_compound"], + "formulas": { + "i_rate_per_period": "r_nominal_annual / m_compounding_periods_per_year", + "n_total_compounding_periods": "t_years * m_compounding_periods_per_year", + "F_compound": "P * (1 + i_rate_per_period)**n_total_compounding_periods" + }, + "required_knowns_for_target": ["P", "r_nominal_annual", "m_compounding_periods_per_year", "t_years"], + "narrative_hooks": ["investment", "deposit", "loan", "future worth", "accumulated amount", "compound interest", "compounded"], + "solution_step_keys": [ + "solution_guidance.identify_knowns", + "solution_guidance.calculate_interest_rate_per_period", + "solution_guidance.calculate_total_periods", + "solution_guidance.state_formula", + "solution_guidance.substitute_values", + "solution_guidance.perform_calculation", + "solution_guidance.final_answer_is" + ] + }, + { + "concept_id": "COMPOUND_INTEREST_SOLVE_FOR_P_FROM_F", + "description": "Calculates the Principal (P) in a compound interest problem, given Future Value, nominal annual interest rate, compounding frequency, and time in years.", + "financial_topic": "Compound Interest", + "target_unknown": "P", + "variables_involved": ["P", "r_nominal_annual", "m_compounding_periods_per_year", "t_years", "i_rate_per_period", "n_total_compounding_periods", "F_compound"], + "formulas": { + "i_rate_per_period": "r_nominal_annual / m_compounding_periods_per_year", + "n_total_compounding_periods": "t_years * m_compounding_periods_per_year", + "P": "F_compound / (1 + i_rate_per_period)**n_total_compounding_periods" + }, + "required_knowns_for_target": ["F_compound", "r_nominal_annual", "m_compounding_periods_per_year", "t_years"], + "narrative_hooks": ["present value", "initial investment", "deposit now", "amount to invest", "compound interest", "compounded"], + "solution_step_keys": [ + "solution_guidance.identify_knowns", + "solution_guidance.calculate_interest_rate_per_period", + "solution_guidance.calculate_total_periods", + "solution_guidance.state_formula", + "solution_guidance.substitute_values", + "solution_guidance.perform_calculation", + "solution_guidance.final_answer_is" + ] + }, + { + "concept_id": "COMPOUND_INTEREST_SOLVE_FOR_RATE", + "description": "Calculates the nominal annual interest rate (r_nominal_annual), given Principal, Future Value, compounding frequency, and time in years.", + "financial_topic": "Compound Interest", + "target_unknown": "r_nominal_annual", + "variables_involved": ["P", "r_nominal_annual", "m_compounding_periods_per_year", "t_years", "i_rate_per_period", "n_total_compounding_periods", "F_compound"], + "formulas": { + "n_total_compounding_periods": "t_years * m_compounding_periods_per_year", + "i_rate_per_period": "(F_compound / P)**(1 / n_total_compounding_periods) - 1", + "r_nominal_annual": "i_rate_per_period * m_compounding_periods_per_year" + }, + "required_knowns_for_target": ["P", "F_compound", "m_compounding_periods_per_year", "t_years"], + "narrative_hooks": ["interest rate", "nominal rate", "rate of return", "compound interest", "compounded"], + "solution_step_keys": [ + "solution_guidance.identify_knowns", + "solution_guidance.calculate_total_periods", + "solution_guidance.state_formula", + "solution_guidance.substitute_values", + "solution_guidance.intermediate_step", + "solution_guidance.perform_calculation", + "solution_guidance.final_answer_is" + ] + }, + { + "concept_id": "COMPOUND_INTEREST_SOLVE_FOR_TIME", + "description": "Calculates the time in years (t_years), given Principal, Future Value, nominal annual interest rate, and compounding frequency. Requires math.log.", + "financial_topic": "Compound Interest", + "target_unknown": "t_years", + "variables_involved": ["P", "r_nominal_annual", "m_compounding_periods_per_year", "t_years", "i_rate_per_period", "n_total_compounding_periods", "F_compound"], + "formulas": { + "i_rate_per_period": "r_nominal_annual / m_compounding_periods_per_year", + "n_total_compounding_periods": "math.log(F_compound / P) / math.log(1 + i_rate_per_period)", + "t_years": "n_total_compounding_periods / m_compounding_periods_per_year" + }, + "required_knowns_for_target": ["P", "F_compound", "r_nominal_annual", "m_compounding_periods_per_year"], + "narrative_hooks": ["time period", "duration", "investment horizon", "how long", "compound interest", "compounded"], + "solution_step_keys": [ + "solution_guidance.identify_knowns", + "solution_guidance.calculate_interest_rate_per_period", + "solution_guidance.state_formula", + "solution_guidance.substitute_values", + "solution_guidance.intermediate_step", + "solution_guidance.perform_calculation", + "solution_guidance.final_answer_is" + ] + }, + { + "concept_id": "EFFECTIVE_RATE_SOLVE_FOR_ER", + "description": "Calculates the Effective Annual Interest Rate (ER), given the nominal annual interest rate and the number of compounding periods per year.", + "financial_topic": "Effective Rate of Interest", + "target_unknown": "ER", + "variables_involved": ["r_nominal_annual", "m_compounding_periods_per_year", "i_rate_per_period", "ER"], + "formulas": { + "i_rate_per_period": "r_nominal_annual / m_compounding_periods_per_year", + "ER": "(1 + i_rate_per_period)**m_compounding_periods_per_year - 1" + }, + "required_knowns_for_target": ["r_nominal_annual", "m_compounding_periods_per_year"], + "narrative_hooks": ["effective rate", "actual annual rate", "true interest rate", "compounding effect"], + "solution_step_keys": [ + "solution_guidance.identify_knowns", + "solution_guidance.calculate_interest_rate_per_period", + "solution_guidance.state_formula", + "solution_guidance.substitute_values", + "solution_guidance.perform_calculation", + "solution_guidance.final_answer_is" + ] + }, + { + "concept_id": "CONTINUOUS_COMPOUNDING_SOLVE_FOR_F", + "description": "Calculates the Future Value (F_continuous) with continuous compounding, given Principal, nominal annual interest rate, and time in years. Requires math.exp.", + "financial_topic": "Continuous Compounding Interest", + "target_unknown": "F_continuous", + "variables_involved": ["P", "r_nominal_annual", "t_years", "F_continuous"], + "formulas": { + "F_continuous": "P * math.exp(r_nominal_annual * t_years)" + }, + "required_knowns_for_target": ["P", "r_nominal_annual", "t_years"], + "narrative_hooks": ["continuous compounding", "investment growth", "future value", "accumulated amount"], + "solution_step_keys": [ + "solution_guidance.identify_knowns", + "solution_guidance.state_formula", + "solution_guidance.substitute_values", + "solution_guidance.perform_calculation", + "solution_guidance.final_answer_is" + ] + }, + { + "concept_id": "CONTINUOUS_COMPOUNDING_SOLVE_FOR_P_FROM_F", + "description": "Calculates the Principal (P) with continuous compounding, given Future Value, nominal annual interest rate, and time in years. Requires math.exp.", + "financial_topic": "Continuous Compounding Interest", + "target_unknown": "P", + "variables_involved": ["P", "r_nominal_annual", "t_years", "F_continuous"], + "formulas": { + "P": "F_continuous / math.exp(r_nominal_annual * t_years)" + }, + "required_knowns_for_target": ["F_continuous", "r_nominal_annual", "t_years"], + "narrative_hooks": ["continuous compounding", "present value", "initial investment", "amount to deposit"], + "solution_step_keys": [ + "solution_guidance.identify_knowns", + "solution_guidance.state_formula", + "solution_guidance.substitute_values", + "solution_guidance.perform_calculation", + "solution_guidance.final_answer_is" + ] + }, + { + "concept_id": "CONTINUOUS_COMPOUNDING_SOLVE_FOR_RATE", + "description": "Calculates the nominal annual interest rate (r_nominal_annual) with continuous compounding, given Principal, Future Value, and time in years. Requires math.log.", + "financial_topic": "Continuous Compounding Interest", + "target_unknown": "r_nominal_annual", + "variables_involved": ["P", "r_nominal_annual", "t_years", "F_continuous"], + "formulas": { + "r_nominal_annual": "math.log(F_continuous / P) / t_years" + }, + "required_knowns_for_target": ["P", "F_continuous", "t_years"], + "narrative_hooks": ["continuous compounding", "interest rate", "nominal rate", "rate of growth"], + "solution_step_keys": [ + "solution_guidance.identify_knowns", + "solution_guidance.state_formula", + "solution_guidance.substitute_values", + "solution_guidance.perform_calculation", + "solution_guidance.final_answer_is" + ] + }, + { + "concept_id": "CONTINUOUS_COMPOUNDING_SOLVE_FOR_TIME", + "description": "Calculates the time in years (t_years) with continuous compounding, given Principal, Future Value, and nominal annual interest rate. Requires math.log.", + "financial_topic": "Continuous Compounding Interest", + "target_unknown": "t_years", + "variables_involved": ["P", "r_nominal_annual", "t_years", "F_continuous"], + "formulas": { + "t_years": "math.log(F_continuous / P) / r_nominal_annual" + }, + "required_knowns_for_target": ["P", "F_continuous", "r_nominal_annual"], + "narrative_hooks": ["continuous compounding", "time period", "duration", "how long to reach value"], + "solution_step_keys": [ + "solution_guidance.identify_knowns", + "solution_guidance.state_formula", + "solution_guidance.substitute_values", + "solution_guidance.perform_calculation", + "solution_guidance.final_answer_is" + ] + }, + { + "concept_id": "CONTINUOUS_COMPOUNDING_EQUIVALENT_SIMPLE_RATE", + "description": "Calculates the equivalent simple interest rate for 1 year for a given nominal annual rate compounded continuously. Requires math.exp.", + "financial_topic": "Continuous Compounding Interest", + "target_unknown": "i_simple_equivalent", + "variables_involved": ["r_nominal_annual", "i_simple_equivalent"], + "formulas": { + "i_simple_equivalent": "math.exp(r_nominal_annual) - 1" + }, + "required_knowns_for_target": ["r_nominal_annual"], + "narrative_hooks": ["continuous compounding", "equivalent simple rate", "comparison rate"], + "solution_step_keys": [ + "solution_guidance.identify_knowns", + "solution_guidance.state_formula", + "solution_guidance.substitute_values", + "solution_guidance.perform_calculation", + "solution_guidance.final_answer_is" + ] + }, + { + "concept_id": "BANKERS_DISCOUNT_SOLVE_FOR_PROCEEDS", + "description": "Calculates the Proceeds (P_proceeds) in a Banker's Discount problem, given Maturity Value (F_maturity), discount rate, and time.", + "financial_topic": "Banker's Discount", + "target_unknown": "P_proceeds", + "variables_involved": ["F_maturity", "d_discount_rate", "t_years", "Db_discount_amount", "P_proceeds"], + "formulas": { + "Db_discount_amount": "F_maturity * d_discount_rate * t_years", + "P_proceeds": "F_maturity - Db_discount_amount" + }, + "required_knowns_for_target": ["F_maturity", "d_discount_rate", "t_years"], + "narrative_hooks": ["banker's discount", "discounted loan", "proceeds", "amount received", "maturity value", "face value"], + "solution_step_keys": [ + "solution_guidance.identify_knowns", + "solution_guidance.convert_time_to_years", + "solution_guidance.state_formula", + "solution_guidance.intermediate_step", + "solution_guidance.substitute_values", + "solution_guidance.perform_calculation", + "solution_guidance.final_answer_is" + ] + }, + { + "concept_id": "BANKERS_DISCOUNT_SOLVE_FOR_DISCOUNT_RATE", + "description": "Calculates the discount rate (d_discount_rate) in a Banker's Discount problem, given Maturity Value, Proceeds, and time.", + "financial_topic": "Banker's Discount", + "target_unknown": "d_discount_rate", + "variables_involved": ["F_maturity", "d_discount_rate", "t_years", "Db_discount_amount", "P_proceeds"], + "formulas": { + "Db_discount_amount": "F_maturity - P_proceeds", + "d_discount_rate": "Db_discount_amount / (F_maturity * t_years)" + }, + "required_knowns_for_target": ["F_maturity", "P_proceeds", "t_years"], + "narrative_hooks": ["banker's discount", "discount rate", "rate of discount", "proceeds", "maturity value"], + "solution_step_keys": [ + "solution_guidance.identify_knowns", + "solution_guidance.convert_time_to_years", + "solution_guidance.intermediate_step", + "solution_guidance.state_formula", + "solution_guidance.substitute_values", + "solution_guidance.perform_calculation", + "solution_guidance.final_answer_is" + ] + }, + { + "concept_id": "BANKERS_DISCOUNT_SOLVE_FOR_SIMPLE_INTEREST_EQUIVALENT", + "description": "Calculates the equivalent simple interest rate (i_simple_equivalent) for a Banker's Discount scenario, based on Proceeds.", + "financial_topic": "Banker's Discount", + "target_unknown": "i_simple_equivalent", + "variables_involved": ["F_maturity", "t_years", "Db_discount_amount", "P_proceeds", "i_simple_equivalent"], + "formulas": { + "Db_discount_amount": "F_maturity - P_proceeds", + "i_simple_equivalent": "Db_discount_amount / (P_proceeds * t_years)" + }, + "required_knowns_for_target": ["F_maturity", "P_proceeds", "t_years"], + "narrative_hooks": ["banker's discount", "equivalent simple interest", "comparison rate", "true cost of borrowing"], + "solution_step_keys": [ + "solution_guidance.identify_knowns", + "solution_guidance.convert_time_to_years", + "solution_guidance.intermediate_step", + "solution_guidance.state_formula", + "solution_guidance.substitute_values", + "solution_guidance.perform_calculation", + "solution_guidance.final_answer_is" + ] + }, + { + "concept_id": "EXACT_SIMPLE_INTEREST_SOLVE_FOR_I", + "description": "Calculates the Exact Simple Interest amount (I_exact_simple), given Principal, annual simple interest rate, start date, and end date.", + "financial_topic": "Exact Simple Interest", + "target_unknown": "I_exact_simple", + "variables_involved": ["P", "i_simple_annual", "start_date", "end_date", "n_time_days", "time_base_days", "n_time_years_fractional", "I_exact_simple"], + "formulas": { + "n_time_years_fractional": "n_time_days / time_base_days", + "I_exact_simple": "P * i_simple_annual * n_time_years_fractional" + }, + "required_knowns_for_target": ["P", "i_simple_annual", "start_date", "end_date"], + "narrative_hooks": ["exact simple interest", "loan interest", "investment earnings", "specific period"], + "solution_step_keys": [ + "solution_guidance.identify_knowns", + "solution_guidance.days_in_period", + "solution_guidance.check_leap_year", + "solution_guidance.determine_time_base_exact", + "solution_guidance.calculate_n_time_years_fractional", + "solution_guidance.state_formula", + "solution_guidance.substitute_values", + "solution_guidance.perform_calculation", + "solution_guidance.final_answer_is" + ] + }, + { + "concept_id": "EXACT_SIMPLE_INTEREST_SOLVE_FOR_F", + "description": "Calculates the Future Value (F_exact_simple) with Exact Simple Interest, given Principal, annual simple interest rate, start date, and end date.", + "financial_topic": "Exact Simple Interest", + "target_unknown": "F_exact_simple", + "variables_involved": ["P", "i_simple_annual", "start_date", "end_date", "n_time_days", "time_base_days", "n_time_years_fractional", "F_exact_simple"], + "formulas": { + "n_time_years_fractional": "n_time_days / time_base_days", + "F_exact_simple": "P * (1 + i_simple_annual * n_time_years_fractional)" + }, + "required_knowns_for_target": ["P", "i_simple_annual", "start_date", "end_date"], + "narrative_hooks": ["exact simple interest", "maturity value", "future amount", "specific period"], + "solution_step_keys": [ + "solution_guidance.identify_knowns", + "solution_guidance.days_in_period", + "solution_guidance.check_leap_year", + "solution_guidance.determine_time_base_exact", + "solution_guidance.calculate_n_time_years_fractional", + "solution_guidance.state_formula", + "solution_guidance.substitute_values", + "solution_guidance.perform_calculation", + "solution_guidance.final_answer_is" + ] + }, + { + "concept_id": "ORDINARY_SIMPLE_INTEREST_SOLVE_FOR_I", + "description": "Calculates the Ordinary Simple Interest amount (I_ordinary_simple), given Principal, annual simple interest rate, start date, and end date.", + "financial_topic": "Ordinary Simple Interest", + "target_unknown": "I_ordinary_simple", + "variables_involved": ["P", "i_simple_annual", "start_date", "end_date", "n_time_days", "time_base_days", "n_time_years_fractional", "I_ordinary_simple"], + "formulas": { + "n_time_years_fractional": "n_time_days / time_base_days", + "I_ordinary_simple": "P * i_simple_annual * n_time_years_fractional" + }, + "required_knowns_for_target": ["P", "i_simple_annual", "start_date", "end_date"], + "narrative_hooks": ["ordinary simple interest", "loan interest", "investment earnings", "360 day year"], + "solution_step_keys": [ + "solution_guidance.identify_knowns", + "solution_guidance.days_in_period", + "solution_guidance.determine_time_base_ordinary", + "solution_guidance.calculate_n_time_years_fractional", + "solution_guidance.state_formula", + "solution_guidance.substitute_values", + "solution_guidance.perform_calculation", + "solution_guidance.final_answer_is" + ] + }, + { + "concept_id": "ORDINARY_SIMPLE_INTEREST_SOLVE_FOR_F", + "description": "Calculates the Future Value (F_ordinary_simple) with Ordinary Simple Interest, given Principal, annual simple interest rate, start date, and end date.", + "financial_topic": "Ordinary Simple Interest", + "target_unknown": "F_ordinary_simple", + "variables_involved": ["P", "i_simple_annual", "start_date", "end_date", "n_time_days", "time_base_days", "n_time_years_fractional", "F_ordinary_simple"], + "formulas": { + "n_time_years_fractional": "n_time_days / time_base_days", + "F_ordinary_simple": "P * (1 + i_simple_annual * n_time_years_fractional)" + }, + "required_knowns_for_target": ["P", "i_simple_annual", "start_date", "end_date"], + "narrative_hooks": ["ordinary simple interest", "maturity value", "future amount", "360 day year"], + "solution_step_keys": [ + "solution_guidance.identify_knowns", + "solution_guidance.days_in_period", + "solution_guidance.determine_time_base_ordinary", + "solution_guidance.calculate_n_time_years_fractional", + "solution_guidance.state_formula", + "solution_guidance.substitute_values", + "solution_guidance.perform_calculation", + "solution_guidance.final_answer_is" + ] + } +] diff --git a/data/names.json b/data/names.json new file mode 100644 index 0000000..c2d6c88 --- /dev/null +++ b/data/names.json @@ -0,0 +1,69 @@ +{ + "persons_titles": ["Mr.", "Ms.", "Engr.", "Dr.", "Prof."], + "persons_first_names": [ + "James", "Mary", "John", "Patricia", "Robert", "Jennifer", "Michael", "Linda", + "William", "Elizabeth", "David", "Barbara", "Richard", "Susan", "Joseph", "Jessica", + "Thomas", "Sarah", "Charles", "Karen", "Christopher", "Nancy", "Daniel", "Lisa", + "Matthew", "Betty", "Anthony", "Margaret", "Mark", "Sandra", "Juan", "Maria", + "Jose", "Ana", "Carlos", "Sofia" + ], + "persons_last_names": [ + "Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", + "Rodriguez", "Martinez", "Hernandez", "Lopez", "Gonzalez", "Wilson", "Anderson", + "Thomas", "Taylor", "Moore", "Jackson", "Martin", "Lee", "Perez", "Thompson", + "White", "Harris", "Sanchez", "Clark", "Ramirez", "Lewis", "Robinson", "Walker", + "Young", "Allen", "King", "Wright", "Scott", "Green", "Baker", "Adams", "Nelson", + "Hill", "Campbell", "Mitchell", "Roberts", "Carter", "Phillips", "Evans", "Turner", + "Torres", "Parker", "Collins", "Edwards", "Stewart", "Flores", "Morris", "Nguyen", + "Murphy", "Rivera", "Cook", "Rogers", "Morgan", "Peterson", "Cooper", "Reed", + "Bailey", "Bell", "Gomez", "Kelly", "Howard", "Ward", "Cox", "Diaz", "Richardson", + "Wood", "Watson", "Brooks", "Bennett", "Gray", "James", "Reyes", "Cruz", "Hughes", + "Price", "Myers", "Long", "Foster", "Sanders", "Ross", "Morales", "Powell", + "Sullivan", "Russell", "Ortiz", "Jenkins", "Gutierrez", "Perry", "Butler", + "Barnes", "Fisher", "Henderson", "Coleman", "Simmons", "Patterson", "Jordan", + "Reynolds", "Hamilton", "Graham", "Kim", "Gonzales", "Alexander", "Ramos", + "Wallace", "Griffin", "West", "Cole", "Hayes", "Chavez", "Gibson", "Bryant", + "Ellis", "Stevens", "Murray", "Ford", "Marshall", "Owens", "McDonald", "Harrison", + "Ruiz", "Kennedy", "Wells", "Alvarez", "Woods", "Mendoza", "Castillo", "Olson", + "Webb", "Washington", "Tucker", "Freeman", "Burns", "Henry", "Vasquez", "Snyder", + "Simpson", "Crawford", "Jimenez", "Porter", "Mason", "Shaw", "Gordon", "Wagner", + "Hunter", "Romero", "Hicks", "Dixon", "Hunt", "Palmer", "Robertson", "Black", + "Holmes", "Stone", "Meyer", "Boyd", "Mills", "Warren", "Fox", "Rose", "Rice", + "Moreno", "Schmidt", "Patel", "Ferguson", "Nichols", "Herrera", "Medina", + "Ryan", "Fernandez", "Weaver", "Daniels", "Stephens", "Gardner", "Payne", + "Kelley", "Dunn", "Pierce", "Arnold", "Tran", "Spencer", "Peters", "Hawkins", + "Grant", "Hansen", "Castro", "Hoffman", "Hart", "Elliott", "Cunningham", + "Knight", "Bradley", "Santos" + ], + "companies_generic_prefix": [ + "Apex", "Vertex", "Zenith", "Nova", "Orion", "Quantum", "Synergy", "Global", + "United", "Dynamic", "Prime", "Core", "Alpha", "Beta", "Omega", "Delta", + "Stellar", "Pinnacle", "Summit", "Horizon", "Matrix", "Nexus", "Catalyst" + ], + "companies_generic_suffix": [ + "Solutions", "Enterprises", "Group", "Corp.", "Inc.", "LLC", "Technologies", + "Systems", "Dynamics", "Innovations", "Ventures", "Holdings", "Logistics", + "Manufacturing", "Consulting", "Services", "Industries", "Partners" + ], + "companies_industry_specific": { + "finance": ["Capital", "Financial", "Investment Group", "Bank", "Credit Union"], + "tech": ["Tech", "Software", "Digital", "Cybernetics", "AI Solutions"], + "construction": ["Builders", "Construction Co.", "Development", "Contractors"], + "retail": ["Goods", "Emporium", "Mart", "Retailers", "Supply Co."] + }, + "items_loan_general": [ + "a business expansion", "a new equipment purchase", "a property acquisition", + "working capital", "a personal project", "debt consolidation", "a vehicle", + "home improvement", "education fees", "a startup venture" + ], + "items_investment_general": [ + "stocks", "bonds", "a mutual fund", "real estate", "a new business", + "a savings account", "a certificate of deposit", "a retirement fund", + "a tech startup", "a portfolio of assets" + ], + "project_names": [ + "Project Alpha", "The Phoenix Initiative", "Operation Starlight", "Blue Sky Project", + "Quantum Leap Program", "Project Chimera", "The Vanguard Project", "Odyssey Plan", + "Project Nova", "Titan Development" + ] +} diff --git a/data/text_snippets.json b/data/text_snippets.json new file mode 100644 index 0000000..388df60 --- /dev/null +++ b/data/text_snippets.json @@ -0,0 +1,127 @@ +{ + "actors_person": ["{person_title} {person_first_name} {person_last_name}", "{person_first_name} {person_last_name}", "{person_title} {person_last_name}"], + "actors_company": ["{company_prefix} {company_suffix}", "{company_prefix} {company_industry}"], + + "actions_loan_present_singular": ["borrows", "takes out a loan for", "secures financing for", "needs a loan of", "is seeking a loan of"], + "actions_loan_present_plural": ["borrow", "take out a loan for", "secure financing for", "need a loan of", "are seeking a loan of"], + "actions_loan_past_singular": ["borrowed", "took out a loan for", "secured financing for", "received a loan of"], + "actions_loan_past_plural": ["borrowed", "took out a loan for", "secured financing for", "received a loan of"], + + "actions_investment_present_singular": ["invests", "deposits", "puts", "plans to invest", "wants to deposit"], + "actions_investment_present_plural": ["invest", "deposit", "put", "plan to invest", "want to deposit"], + "actions_investment_past_singular": ["invested", "deposited", "put", "made an investment of"], + "actions_investment_past_plural": ["invested", "deposited", "put", "made an investment of"], + + "actions_repayment_present_singular": ["repays", "settles", "amortizes", "makes a payment on"], + "actions_repayment_present_plural": ["repay", "settle", "amortize", "make a payment on"], + "actions_repayment_past_singular": ["repaid", "settled", "amortized", "made a payment on"], + "actions_repayment_past_plural": ["repaid", "settled", "amortized", "made a payment on"], + + "actions_receive_present_singular": ["receives", "obtains", "gets", "is due to receive"], + "actions_receive_present_plural": ["receive", "obtain", "get", "are due to receive"], + "actions_receive_past_singular": ["received", "obtained", "got"], + "actions_receive_past_plural": ["received", "obtained", "got"], + + "actions_earn_present_singular": ["earns", "accumulates", "yields"], + "actions_earn_present_plural": ["earn", "accumulate", "yield"], + "actions_earn_past_singular": ["earned", "accumulated", "yielded"], + "actions_earn_past_plural": ["earned", "accumulated", "yielded"], + + "time_phrases_duration": ["for a period of", "over", "for", "during"], + "time_phrases_point": ["at the end of", "after", "in"], + + "rate_phrases": ["at a rate of", "with an interest of", "at an annual rate of", "earning interest at"], + "compounding_phrases": ["compounded {compounding_frequency_adverb}", "with {compounding_frequency_adverb} compounding"], + + "purpose_phrases_loan": ["for {item_loan}", "to finance {item_loan}", "to purchase {item_loan}"], + "purpose_phrases_investment": ["in {item_investment}", "into {item_investment}", "to grow their capital through {item_investment}"], + + "question_starters_what_is": ["What is the", "Determine the", "Calculate the", "Find the", "What will be the", "What was the"], + "question_starters_how_much": ["How much is the", "How much will be the", "How much was the", "How much should be"], + "question_starters_how_long": ["How long will it take for", "How many years are needed for", "What is the time period for"], + + "scenario_introductions": [ + "Consider a scenario where {actor}", + "{actor} is planning to", + "Suppose {actor}", + "Imagine {actor} needs to" + ], + "scenario_connectors": [ + "The terms of the agreement state that", + "It is known that", + "Given that", + "Assuming that" + ], + "scenario_closures_question_prefix": [ + "Based on this information,", + "Therefore,", + "With these conditions," + ], + + "solution_guidance": { + "identify_knowns": "First, let's identify the given values (knowns) in the problem:", + "state_formula": "The relevant formula for this problem is:", + "substitute_values": "Now, we substitute the known values into the formula:", + "perform_calculation": "Performing the calculation:", + "intermediate_step": "The intermediate result for {step_name} is:", + "final_answer_is": "Therefore, the {unknown_variable_description} is:", + "convert_time_to_years": "Convert the time period to years: {original_time_value} {original_time_unit} = {converted_time_value_years} years.", + "calculate_interest_rate_per_period": "Calculate the interest rate per compounding period (i): i = r / m = {nominal_rate_decimal} / {compounding_periods_per_year} = {interest_rate_per_period_decimal}.", + "calculate_total_periods": "Calculate the total number of compounding periods (n): n = t * m = {time_in_years} years * {compounding_periods_per_year} = {total_periods} periods.", + "check_leap_year": "{year} is {is_or_is_not} a leap year.", + "days_in_period": "The number of days from {start_date} to {end_date} is {number_of_days} days." + }, + + "variable_descriptions": { + "P": "principal amount", + "F": "future value", + "I": "interest amount", + "i_simple_annual": "annual simple interest rate", + "n_time_years": "time period in years", + "n_time_months": "time period in months", + "n_time_days": "time period in days", + "r_nominal_annual": "nominal annual interest rate", + "m_compounding_periods_per_year": "number of compounding periods per year", + "i_rate_per_period": "interest rate per compounding period", + "n_total_compounding_periods": "total number of compounding periods", + "ER": "effective interest rate", + "Db": "banker's discount amount", + "d_discount_rate": "discount rate", + "Proceeds": "proceeds from the loan", + "I_exact_simple": "exact simple interest amount", + "F_exact_simple": "future value with exact simple interest", + "I_ordinary_simple": "ordinary simple interest amount", + "F_ordinary_simple": "future value with ordinary simple interest", + "start_date": "start date of the period", + "end_date": "end date of the period", + "time_base_days": "day count basis for the year (time base)", + "n_time_years_fractional": "time period as a fraction of a year" + }, + + "solution_guidance": { + "identify_knowns": "First, let's identify the given values (knowns) in the problem:", + "state_formula": "The relevant formula for this problem is:", + "substitute_values": "Now, we substitute the known values into the formula:", + "perform_calculation": "Performing the calculation:", + "intermediate_step": "The intermediate result for {step_name} is:", + "final_answer_is": "Therefore, the {unknown_variable_description} is:", + "convert_time_to_years": "Convert the time period to years: {original_time_value} {original_time_unit} = {converted_time_value_years} years.", + "calculate_interest_rate_per_period": "Calculate the interest rate per compounding period (i): i = r / m = {nominal_rate_decimal} / {compounding_periods_per_year} = {interest_rate_per_period_decimal}.", + "calculate_total_periods": "Calculate the total number of compounding periods (n): n = t * m = {time_in_years} years * {compounding_periods_per_year} = {total_periods} periods.", + "check_leap_year": "{year} is {is_or_is_not} a leap year.", + "days_in_period": "The number of days from {start_date} to {end_date} is {number_of_days} days.", + "determine_time_base_exact": "For exact simple interest, the time base is the actual number of days in the reference year ({year}), which is {days_in_year} days.", + "determine_time_base_ordinary": "For ordinary simple interest, the time base is 360 days.", + "calculate_n_time_years_fractional": "Calculate the time as a fraction of a year (t_fractional): t_fractional = number of days / time base = {n_time_days} / {time_base_days} = {n_time_years_fractional_value}." + }, + + "compounding_frequency_adverbs": { + "annually": "annually", + "semi-annually": "semi-annually", + "quarterly": "quarterly", + "monthly": "monthly", + "bi-monthly": "bi-monthly", + "semi-monthly": "semi-monthly", + "continuously": "continuously" + } +} diff --git a/data/value_ranges.json b/data/value_ranges.json new file mode 100644 index 0000000..a657d9e --- /dev/null +++ b/data/value_ranges.json @@ -0,0 +1,137 @@ +{ + "principal": { + "min": 500, + "max": 200000, + "currency": "Php", + "decimals": 2, + "default_display_precision": 2 + }, + "loan_amount": { + "min": 1000, + "max": 500000, + "currency": "Php", + "decimals": 2, + "default_display_precision": 2 + }, + "investment_amount": { + "min": 100, + "max": 100000, + "currency": "Php", + "decimals": 2, + "default_display_precision": 2 + }, + "future_value": { + "min": 1000, + "max": 1000000, + "currency": "Php", + "decimals": 2, + "default_display_precision": 2 + }, + "interest_amount": { + "min": 50, + "max": 50000, + "currency": "Php", + "decimals": 2, + "default_display_precision": 2 + }, + "payment_amount": { + "min": 100, + "max": 50000, + "currency": "Php", + "decimals": 2, + "default_display_precision": 2 + }, + "simple_interest_rate_annual": { + "min": 0.02, + "max": 0.25, + "unit_display": "% per annum", + "internal_precision": 8, + "display_precision": 2 + }, + "compound_interest_rate_nominal": { + "min": 0.03, + "max": 0.18, + "unit_display": "%", + "internal_precision": 8, + "display_precision": 2 + }, + "discount_rate_bankers": { + "min": 0.05, + "max": 0.20, + "unit_display": "%", + "internal_precision": 8, + "display_precision": 2 + }, + "time_years": { + "min": 1, + "max": 20, + "unit": "years", + "integer": true, + "allow_fractional_if_months_also_present": true + }, + "time_months": { + "min": 1, + "max": 48, + "unit": "months", + "integer": true + }, + "time_days": { + "min": 10, + "max": 360, + "unit": "days", + "integer": true + }, + "number_of_periods_general": { + "min": 2, + "max": 30, + "unit": "periods", + "integer": true + }, + "loan_payment_count": { + "min": 2, + "max": 5, + "integer": true + }, + "compounding_frequency_options": { + "annually": 1, + "semi-annually": 2, + "quarterly": 4, + "monthly": 12, + "bi-monthly": 6, + "semi-monthly": 24 + }, + "gradient_amount": { + "min": 100, + "max": 5000, + "currency": "Php", + "decimals": 2, + "default_display_precision": 2 + }, + "date_general": { + "description": "General date, used for start_date and end_date generation.", + "type": "date", + "min_year": 1990, + "max_year": 2030 + }, + "time_days_exact_ordinary": { + "description": "Number of days for exact/ordinary interest calculations (derived from dates).", + "type": "integer", + "min": 30, + "max": 730, + "unit": "days", + "display_precision": 0 + }, + "time_base_days_exact_ordinary": { + "description": "Time base in days for exact (365/366) or ordinary (360) interest.", + "type": "integer", + "options": [360, 365, 366], + "unit": "days", + "display_precision": 0 + }, + "time_years_fractional": { + "description": "Time period as a fraction of a year (calculated).", + "type": "float", + "unit": "years", + "display_precision": 6 + } +} diff --git a/main.py b/main.py new file mode 100644 index 0000000..f050ff1 --- /dev/null +++ b/main.py @@ -0,0 +1,6 @@ +def main(): + print("Hello from problem-generator!") + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..84a7d74 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "problem-generator" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.9" +dependencies = [] diff --git a/src/data_loader.py b/src/data_loader.py new file mode 100644 index 0000000..6c0e443 --- /dev/null +++ b/src/data_loader.py @@ -0,0 +1,63 @@ +import json +import os + +DATA_DIR = os.path.join(os.path.dirname(__file__), '..', 'data') +BUILDING_BLOCKS_DIR = os.path.join(os.path.dirname(__file__), '..', 'building_blocks') + +def load_json_file(file_path): + """Loads a JSON file from the given path.""" + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + return data + except FileNotFoundError: + print(f"Error: File not found at {file_path}") + return None + except json.JSONDecodeError: + print(f"Error: Could not decode JSON from {file_path}") + return None + except Exception as e: + print(f"An unexpected error occurred while loading {file_path}: {e}") + return None + +def get_value_ranges(): + """Loads value_ranges.json data.""" + file_path = os.path.join(DATA_DIR, 'value_ranges.json') + return load_json_file(file_path) + +def get_names_data(): + """Loads names.json data.""" + file_path = os.path.join(DATA_DIR, 'names.json') + return load_json_file(file_path) + +def get_text_snippets(): + """Loads text_snippets.json data.""" + file_path = os.path.join(DATA_DIR, 'text_snippets.json') + return load_json_file(file_path) + +def get_financial_concepts(): + """Loads financial_concepts.json data.""" + file_path = os.path.join(BUILDING_BLOCKS_DIR, 'financial_concepts.json') + return load_json_file(file_path) + +if __name__ == '__main__': + # Test functions + value_ranges = get_value_ranges() + if value_ranges: + print("Successfully loaded value_ranges.json") + # print(json.dumps(value_ranges, indent=2)) + + names_data = get_names_data() + if names_data: + print("Successfully loaded names.json") + # print(json.dumps(names_data, indent=2)) + + text_snippets = get_text_snippets() + if text_snippets: + print("Successfully loaded text_snippets.json") + # print(json.dumps(text_snippets, indent=2)) + + financial_concepts = get_financial_concepts() + if financial_concepts: + print("Successfully loaded financial_concepts.json") + # print(json.dumps(financial_concepts, indent=2)) diff --git a/src/date_utils.py b/src/date_utils.py new file mode 100644 index 0000000..542e00d --- /dev/null +++ b/src/date_utils.py @@ -0,0 +1,133 @@ +from datetime import date, timedelta +import random + +def is_leap_year(year): + """Determines if a year is a leap year.""" + return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0) + +def days_in_year(year): + """Returns the number of days in a given year.""" + return 366 if is_leap_year(year) else 365 + +def days_in_month(year, month): + """Returns the number of days in a specific month of a year.""" + if month == 2: + return 29 if is_leap_year(year) else 28 + elif month in [4, 6, 9, 11]: + return 30 + else: + return 31 + +def get_random_date(start_year=2000, end_year=2030): + """Generates a random date within a given year range.""" + year = random.randint(start_year, end_year) + month = random.randint(1, 12) + day = random.randint(1, days_in_month(year, month)) + return date(year, month, day) + +def get_random_date_period(min_days=30, max_days=730, base_year_range=(1990, 2025)): + """ + Generates a random start date and an end date that is min_days to max_days after the start date. + Returns (start_date, end_date, number_of_days). + """ + start_date = get_random_date(base_year_range[0], base_year_range[1]) + num_days_in_period = random.randint(min_days, max_days) + end_date = start_date + timedelta(days=num_days_in_period) + + # The actual number of days between two dates, inclusive of start and exclusive of end, + # or inclusive of both if counting "duration". For interest calculations, it's often + # the difference as calculated by date objects. + # For exact simple interest, the problem usually implies counting the actual days in the period. + # timedelta already gives the difference. If the problem implies "from date X to date Y inclusive", + # then it might be (end_date - start_date).days + 1. + # However, the sample problems (e.g. Dec 27 to Mar 23) count days like: + # Dec: 31-27 = 4. Jan: 31. Feb: 28. Mar: 23. Total = 86. + # (date(2003,3,23) - date(2002,12,27)).days = 86. So timedelta is correct. + + actual_days_difference = (end_date - start_date).days + + return start_date, end_date, actual_days_difference + +def calculate_exact_days(start_date_obj, end_date_obj): + """ + Calculates the exact number of days between two date objects. + This is consistent with how financial day counts are often done. + """ + if not isinstance(start_date_obj, date) or not isinstance(end_date_obj, date): + raise ValueError("Inputs must be datetime.date objects.") + if start_date_obj > end_date_obj: + raise ValueError("Start date cannot be after end date.") + + return (end_date_obj - start_date_obj).days + +def format_date_for_display(date_obj): + """Formats a date object as 'Month Day, Year' (e.g., January 1, 2023).""" + if not isinstance(date_obj, date): + return str(date_obj) + return date_obj.strftime("%B %d, %Y") + + +if __name__ == '__main__': + print("Testing Date Utilities:") + + # Test is_leap_year + print(f"\n--- Testing is_leap_year ---") + print(f"Is 2000 a leap year? {is_leap_year(2000)} (Expected: True)") + assert is_leap_year(2000) + print(f"Is 1900 a leap year? {is_leap_year(1900)} (Expected: False)") + assert not is_leap_year(1900) + print(f"Is 2023 a leap year? {is_leap_year(2023)} (Expected: False)") + assert not is_leap_year(2023) + print(f"Is 2024 a leap year? {is_leap_year(2024)} (Expected: True)") + assert is_leap_year(2024) + + # Test days_in_year + print(f"\n--- Testing days_in_year ---") + print(f"Days in 2023: {days_in_year(2023)} (Expected: 365)") + assert days_in_year(2023) == 365 + print(f"Days in 2024: {days_in_year(2024)} (Expected: 366)") + assert days_in_year(2024) == 366 + + # Test days_in_month + print(f"\n--- Testing days_in_month ---") + print(f"Days in Feb 2023: {days_in_month(2023, 2)} (Expected: 28)") + assert days_in_month(2023, 2) == 28 + print(f"Days in Feb 2024: {days_in_month(2024, 2)} (Expected: 29)") + assert days_in_month(2024, 2) == 29 + print(f"Days in Apr 2023: {days_in_month(2023, 4)} (Expected: 30)") + assert days_in_month(2023, 4) == 30 + print(f"Days in Jan 2023: {days_in_month(2023, 1)} (Expected: 31)") + assert days_in_month(2023, 1) == 31 + + # Test get_random_date + print(f"\n--- Testing get_random_date ---") + for _ in range(3): + rd = get_random_date(2020, 2022) + print(f"Random date: {rd} ({format_date_for_display(rd)})") + assert 2020 <= rd.year <= 2022 + + # Test get_random_date_period + print(f"\n--- Testing get_random_date_period ---") + for _ in range(3): + start, end, num_days = get_random_date_period(min_days=10, max_days=90) + print(f"Period: {format_date_for_display(start)} to {format_date_for_display(end)}, Days: {num_days}") + assert 10 <= num_days <= 90 + assert (end - start).days == num_days + + # Test calculate_exact_days (matches sample problem from notes) + # Problem 3: December 27, 2002 to March 23, 2003 -> 86 days + print(f"\n--- Testing calculate_exact_days (Sample Problem) ---") + d1 = date(2002, 12, 27) + d2 = date(2003, 3, 23) + exact_days_sample = calculate_exact_days(d1, d2) + print(f"Days from {format_date_for_display(d1)} to {format_date_for_display(d2)}: {exact_days_sample} (Expected: 86)") + assert exact_days_sample == 86 + + # Problem 5: February 14, 1984 to November 30, 1984 -> 290 days (1984 is leap) + d3 = date(1984, 2, 14) + d4 = date(1984, 11, 30) + exact_days_sample2 = calculate_exact_days(d3, d4) + print(f"Days from {format_date_for_display(d3)} to {format_date_for_display(d4)}: {exact_days_sample2} (Expected: 290)") + assert exact_days_sample2 == 290 + + print("\nAll date utility tests passed if no assertion errors.") diff --git a/src/formula_evaluator.py b/src/formula_evaluator.py new file mode 100644 index 0000000..7947426 --- /dev/null +++ b/src/formula_evaluator.py @@ -0,0 +1,123 @@ +import math +import decimal + +# Define a context for safe evaluation +# Only include necessary math functions and constants +SAFE_MATH_CONTEXT = { + "math": math, # Provides access to math.log, math.exp, math.pow, etc. + "Decimal": decimal.Decimal, # For precise arithmetic if needed + # Standard operators (+, -, *, /, **) are inherently available. + # Built-in functions like round() are also available. +} + +def evaluate_formula(formula_str, context_vars): + """ + Safely evaluates a mathematical formula string using a given context of variables. + + Args: + formula_str (str): The formula to evaluate (e.g., "P * (1 + i * n)"). + context_vars (dict): A dictionary of variable names and their numerical values + (e.g., {"P": 1000, "i": 0.05, "n": 2}). + + Returns: + The result of the evaluation, or None if an error occurs. + """ + if not isinstance(formula_str, str): + print(f"Error: Formula string must be a string, got {type(formula_str)}") + return None + if not isinstance(context_vars, dict): + print(f"Error: Context variables must be a dictionary, got {type(context_vars)}") + return None + + # Combine the safe math context with the problem-specific variables + # Problem-specific variables can override items in SAFE_MATH_CONTEXT if names clash, + # but this is generally not expected for simple variable names like P, F, i, n. + evaluation_context = {**SAFE_MATH_CONTEXT, **context_vars} + + try: + # Using eval() here, which is generally risky if formula_str is from an untrusted source. + # However, in this application, formula_str comes from our own controlled + # financial_concepts.json file, and evaluation_context is restricted. + result = eval(formula_str, {"__builtins__": {}}, evaluation_context) + + # Ensure result is a standard float or int if it's a Decimal, for consistency + if isinstance(result, decimal.Decimal): + return float(result) + return result + except NameError as e: + print(f"Error evaluating formula '{formula_str}': Variable not defined - {e}") + print(f"Available context keys: {list(evaluation_context.keys())}") + return None + except TypeError as e: + print(f"Error evaluating formula '{formula_str}': Type error - {e}") + return None + except ZeroDivisionError: + print(f"Error evaluating formula '{formula_str}': Division by zero.") + return None + except Exception as e: + print(f"An unexpected error occurred while evaluating formula '{formula_str}': {e}") + return None + +if __name__ == '__main__': + print("Testing Formula Evaluator:") + + # Test Case 1: Simple Interest Future Value + formula1 = "P * (1 + i * n)" + context1 = {"P": 1000, "i": 0.05, "n": 2} + result1 = evaluate_formula(formula1, context1) + print(f"\nFormula: {formula1}, Context: {context1}") + print(f"Expected: {1000 * (1 + 0.05 * 2)}") + print(f"Actual: {result1}") + assert result1 == 1100.0 + + # Test Case 2: Compound Interest Future Value + formula2 = "P * (1 + i)**n" + context2 = {"P": 5000, "i": 0.02, "n": 10} # e.g. 8% quarterly for 2.5 years -> i=0.08/4=0.02, n=2.5*4=10 + result2 = evaluate_formula(formula2, context2) + print(f"\nFormula: {formula2}, Context: {context2}") + expected2 = 5000 * (1 + 0.02)**10 + print(f"Expected: {expected2}") + print(f"Actual: {result2}") + assert abs(result2 - expected2) < 1e-9 # Compare floats with tolerance + + # Test Case 3: Using math.log (e.g., solving for n in compound interest) + # n = math.log(F / P) / math.log(1 + i) + formula3 = "math.log(F / P) / math.log(1 + i)" + context3 = {"F": 6094.972103200001, "P": 5000, "i": 0.02} + result3 = evaluate_formula(formula3, context3) + print(f"\nFormula: {formula3}, Context: {context3}") + expected3 = math.log(6094.972103200001 / 5000) / math.log(1 + 0.02) + print(f"Expected: {expected3}") # Should be close to 10 + print(f"Actual: {result3}") + assert abs(result3 - 10.0) < 1e-9 + + # Test Case 4: Using math.exp (e.g., continuous compounding) + # F = P * math.exp(r * t) + formula4 = "P * math.exp(r * t)" + context4 = {"P": 100, "r": 0.05, "t": 1} + result4 = evaluate_formula(formula4, context4) + print(f"\nFormula: {formula4}, Context: {context4}") + expected4 = 100 * math.exp(0.05 * 1) + print(f"Expected: {expected4}") + print(f"Actual: {result4}") + assert abs(result4 - expected4) < 1e-9 + + # Test Case 5: Undefined variable + formula5 = "P * (1 + interest_rate * n)" # 'interest_rate' not in context + context5 = {"P": 1000, "i": 0.05, "n": 2} + result5 = evaluate_formula(formula5, context5) + print(f"\nFormula: {formula5}, Context: {context5}") + print(f"Expected: None (due to error)") + print(f"Actual: {result5}") + assert result5 is None + + # Test Case 6: Division by zero + formula6 = "P / n" + context6 = {"P": 1000, "n": 0} + result6 = evaluate_formula(formula6, context6) + print(f"\nFormula: {formula6}, Context: {context6}") + print(f"Expected: None (due to error)") + print(f"Actual: {result6}") + assert result6 is None + + print("\nAll tests passed if no assertion errors.") diff --git a/src/narrative_builder.py b/src/narrative_builder.py new file mode 100644 index 0000000..3231636 --- /dev/null +++ b/src/narrative_builder.py @@ -0,0 +1,209 @@ +import random +from src.data_loader import get_names_data, get_text_snippets +from src.value_sampler import format_value_for_display + +# Cache loaded data +NAMES_DATA = None +TEXT_SNIPPETS = None + +def _get_names_data_cached(): + global NAMES_DATA + if NAMES_DATA is None: + NAMES_DATA = get_names_data() + return NAMES_DATA + +def _get_text_snippets_cached(): + global TEXT_SNIPPETS + if TEXT_SNIPPETS is None: + TEXT_SNIPPETS = get_text_snippets() + return TEXT_SNIPPETS + +def get_random_actor(): + """Generates a random actor (person or company).""" + names_data = _get_names_data_cached() + snippets = _get_text_snippets_cached() + + actor_type = random.choice(["person", "company"]) + actor_string_template = "" + + if actor_type == "person": + actor_string_template = random.choice(snippets["actors_person"]) + title = random.choice(names_data["persons_titles"]) + first_name = random.choice(names_data["persons_first_names"]) + last_name = random.choice(names_data["persons_last_names"]) + return actor_string_template.format(person_title=title, person_first_name=first_name, person_last_name=last_name) + else: # company + actor_string_template = random.choice(snippets["actors_company"]) + prefix = random.choice(names_data["companies_generic_prefix"]) + + # Decide if to use generic suffix or industry specific + if random.random() < 0.7: # 70% chance for generic suffix + suffix = random.choice(names_data["companies_generic_suffix"]) + return actor_string_template.format(company_prefix=prefix, company_suffix=suffix, company_industry=suffix) # company_industry for templates that might use it + else: + industry_key = random.choice(list(names_data["companies_industry_specific"].keys())) + industry_suffix = random.choice(names_data["companies_industry_specific"][industry_key]) + return actor_string_template.format(company_prefix=prefix, company_suffix=industry_suffix, company_industry=industry_suffix) + + +def build_narrative(financial_concept, known_values_data, unknown_variable_key): + """ + Constructs a problem narrative dynamically. + + Args: + financial_concept (dict): The financial concept definition. + known_values_data (dict): Dict of known variable data (from value_sampler). + e.g., {"P": {'value': 1000, ...}, "i_simple_annual": {'value': 0.05, ...}} + unknown_variable_key (str): The key of the variable to be solved for (e.g., "F_simple"). + + Returns: + str: The generated problem narrative. + """ + snippets = _get_text_snippets_cached() + actor = get_random_actor() + + narrative_parts = [] + + # 1. Introduction / Scenario Setup + intro_template = random.choice(snippets["scenario_introductions"]) + narrative_parts.append(intro_template.format(actor=actor)) + + # 2. Describe the action (loan, investment) and known amounts + # This part needs to be more intelligent based on the concept and knowns/unknowns + + action_type = "loan" # Default, can be refined by concept hooks + if any(hook in financial_concept.get("narrative_hooks", []) for hook in ["investment", "deposit"]): + action_type = "investment" + + # Choose verb tense (past is common for setting up a problem) + if action_type == "loan": + action_verb_template_group = snippets["actions_loan_past_singular"] # Assuming singular actor for now + else: # investment + action_verb_template_group = snippets["actions_investment_past_singular"] + + action_verb = random.choice(action_verb_template_group) + + # Primary known value (e.g., Principal if F is unknown, or Future Value if P is unknown) + primary_value_key = "" + primary_value_desc = "" + + if "P" in known_values_data and unknown_variable_key.startswith("F"): # Solving for Future Value + primary_value_key = "P" + primary_value_desc = "an initial amount of" + elif "F_simple" in known_values_data and unknown_variable_key == "P": # Solving for Principal from Simple Future + primary_value_key = "F_simple" + primary_value_desc = "a future target of" + action_verb = random.choice(snippets["actions_receive_present_singular"]) # "wants to receive" + elif "F_compound" in known_values_data and unknown_variable_key == "P": # Solving for Principal from Compound Future + primary_value_key = "F_compound" + primary_value_desc = "a future target of" + action_verb = random.choice(snippets["actions_receive_present_singular"]) + # Add more conditions for other unknowns like I_simple, rates, time etc. + + if primary_value_key and primary_value_key in known_values_data: + val_data = known_values_data[primary_value_key] + narrative_parts.append(f"{action_verb} {primary_value_desc} {format_value_for_display(val_data)}") + elif "P" in known_values_data: # Fallback if primary logic missed, describe P if known + narrative_parts.append(f"{action_verb} {format_value_for_display(known_values_data['P'])}") + + + # 3. Add purpose if applicable + if action_type == "loan" and random.random() < 0.5: + item_loan = random.choice(_get_names_data_cached()["items_loan_general"]) + purpose_phrase = random.choice(snippets["purpose_phrases_loan"]).format(item_loan=item_loan) + narrative_parts.append(purpose_phrase) + elif action_type == "investment" and random.random() < 0.5: + item_investment = random.choice(_get_names_data_cached()["items_investment_general"]) + purpose_phrase = random.choice(snippets["purpose_phrases_investment"]).format(item_investment=item_investment) + narrative_parts.append(purpose_phrase) + + # 4. Describe other knowns (rate, time, compounding) + # Simple Interest Rate + if "i_simple_annual" in known_values_data: + rate_phrase = random.choice(snippets["rate_phrases"]) + narrative_parts.append(f"{rate_phrase} {format_value_for_display(known_values_data['i_simple_annual'])}") + + # Nominal Compound Interest Rate + if "r_nominal_annual" in known_values_data: + rate_phrase = random.choice(snippets["rate_phrases"]) + narrative_parts.append(f"{rate_phrase} {format_value_for_display(known_values_data['r_nominal_annual'])}") + if "m_compounding_periods_per_year" in known_values_data: + freq_name = known_values_data["m_compounding_periods_per_year"]["name"] + freq_adverb = snippets["compounding_frequency_adverbs"].get(freq_name, freq_name) + comp_phrase = random.choice(snippets["compounding_phrases"]).format(compounding_frequency_adverb=freq_adverb) + narrative_parts.append(comp_phrase) + + # Time (handle years, months, days - prefer years if available) + time_described = False + if "n_time_years" in known_values_data: + time_phrase = random.choice(snippets["time_phrases_duration"]) + narrative_parts.append(f"{time_phrase} {format_value_for_display(known_values_data['n_time_years'])}") + time_described = True + elif "t_years" in known_values_data: # for compound interest + time_phrase = random.choice(snippets["time_phrases_duration"]) + narrative_parts.append(f"{time_phrase} {format_value_for_display(known_values_data['t_years'])}") + time_described = True + + # If time in months or days is primary and years not directly given (less common for this structure) + if not time_described and "n_time_months" in known_values_data: + time_phrase = random.choice(snippets["time_phrases_duration"]) + narrative_parts.append(f"{time_phrase} {format_value_for_display(known_values_data['n_time_months'])}") + elif not time_described and "n_time_days" in known_values_data: + # For exact simple interest, dates are usually given instead of number of days directly + if "start_date" in known_values_data and "end_date" in known_values_data: + narrative_parts.append(f"from {format_value_for_display(known_values_data['start_date'])} to {format_value_for_display(known_values_data['end_date'])}") + else: # Fallback if only days are given + time_phrase = random.choice(snippets["time_phrases_duration"]) + narrative_parts.append(f"{time_phrase} {format_value_for_display(known_values_data['n_time_days'])}") + + + # 5. Formulate the question about the unknown variable + question_starter = random.choice(snippets["question_starters_what_is"]) # Default + if unknown_variable_key.lower().startswith("p"): + question_starter = random.choice(snippets["question_starters_how_much"]) + elif "time" in unknown_variable_key.lower(): + question_starter = random.choice(snippets["question_starters_how_long"]) + + unknown_desc = snippets.get("variable_descriptions", {}).get(unknown_variable_key, unknown_variable_key.replace("_", " ")) + + question_closure_prefix = random.choice(snippets["scenario_closures_question_prefix"]) + narrative_parts.append(f"{question_closure_prefix} {question_starter.lower()} {unknown_desc}?") + + return " ".join(part.strip() for part in narrative_parts if part).replace(" .", ".").replace(" ?", "?") + "." + + +if __name__ == '__main__': + print("Testing Narrative Builder:") + # Mock data for testing (normally loaded by problem_engine) + mock_concept_simple_f = { + "concept_id": "SIMPLE_INTEREST_SOLVE_FOR_F", + "narrative_hooks": ["loan", "simple interest", "future amount"] + } + mock_known_simple_f = { + "P": {'key': 'P', 'value': 10000.0, 'currency': 'Php', 'display_precision': 2}, + "i_simple_annual": {'key': 'i_simple_annual', 'value': 0.12, 'unit_display': '% per annum', 'display_precision': 2}, + "n_time_years": {'key': 'n_time_years', 'value': 2.0, 'unit': 'years', 'display_precision': 1} + } + + print("\n--- Test Case 1: Simple Interest, Solve for F ---") + narrative1 = build_narrative(mock_concept_simple_f, mock_known_simple_f, "F_simple") + print(narrative1) + + mock_concept_compound_p = { + "concept_id": "COMPOUND_INTEREST_SOLVE_FOR_P_FROM_F", + "narrative_hooks": ["investment", "compound interest", "present value"] + } + mock_known_compound_p = { + "F_compound": {'key': 'F_compound', 'value': 25000.0, 'currency': 'Php', 'display_precision': 2}, + "r_nominal_annual": {'key': 'r_nominal_annual', 'value': 0.08, 'unit_display': '%', 'display_precision': 2}, + "m_compounding_periods_per_year": {'key': 'm_compounding_periods_per_year', 'name': 'quarterly', 'value': 4}, + "t_years": {'key': 't_years', 'value': 5.0, 'unit': 'years', 'display_precision': 1} + } + print("\n--- Test Case 2: Compound Interest, Solve for P ---") + narrative2 = build_narrative(mock_concept_compound_p, mock_known_compound_p, "P") + print(narrative2) + + # Test with a different actor type + print("\n--- Test Case 3: Different Actor ---") + narrative3 = build_narrative(mock_concept_simple_f, mock_known_simple_f, "F_simple") + print(narrative3) diff --git a/src/problem_engine.py b/src/problem_engine.py new file mode 100644 index 0000000..fa267ad --- /dev/null +++ b/src/problem_engine.py @@ -0,0 +1,279 @@ +import random +import math # For direct use if not going through formula_evaluator for simple conversions +from src import data_loader +from src import value_sampler +from src import formula_evaluator +from src import narrative_builder +from src import solution_presenter +from src import date_utils # For potential date-based time calculations + +# --- Cached Data --- +FINANCIAL_CONCEPTS = None +TEXT_SNIPPETS_DATA = None # To avoid conflict with value_sampler's TEXT_SNIPPETS +NAMES_DATA_CACHE = None +VALUE_RANGES_CACHE = None + +def _load_all_data_cached(): + global FINANCIAL_CONCEPTS, TEXT_SNIPPETS_DATA, NAMES_DATA_CACHE, VALUE_RANGES_CACHE + if FINANCIAL_CONCEPTS is None: + FINANCIAL_CONCEPTS = data_loader.get_financial_concepts() + if TEXT_SNIPPETS_DATA is None: + TEXT_SNIPPETS_DATA = data_loader.get_text_snippets() # Loaded for solution_presenter and narrative_builder + if NAMES_DATA_CACHE is None: + NAMES_DATA_CACHE = data_loader.get_names_data() # Loaded for narrative_builder + if VALUE_RANGES_CACHE is None: + VALUE_RANGES_CACHE = data_loader.get_value_ranges() # Loaded for value_sampler + + # Ensure sub-modules also use cached data if they have their own caches + # This is already handled by them checking their global cache variables. + +# --- Variable Mapping --- +# Maps concept variable names to value_ranges.json keys +# This might need to be more dynamic or extensive based on concept variations. +CONCEPT_VAR_TO_VALUERANGE_KEY = { + "P": "principal", # Could also be loan_amount, investment_amount + "F_simple": "future_value", + "F_compound": "future_value", + "F_continuous": "future_value", + "F_maturity": "future_value", # For Banker's Discount, F is the maturity value + "I_simple": "interest_amount", + "Db_discount_amount": "interest_amount", # Or a specific "discount_amount" range + "P_proceeds": "principal", # Proceeds are a present value + + "i_simple_annual": "simple_interest_rate_annual", + "n_time_years": "time_years", # For simple interest + "n_time_months": "time_months", + "n_time_days": "time_days", + + "r_nominal_annual": "compound_interest_rate_nominal", + "t_years": "time_years", # For compound/continuous interest time + # m_compounding_periods_per_year is handled specially + "ER": "compound_interest_rate_nominal", # Effective rate is a rate + "d_discount_rate": "discount_rate_bankers", + "i_simple_equivalent": "simple_interest_rate_annual" # for comparison rates +} + +def generate_problem(): + """ + Generates a complete financial math problem with narrative and solution. + """ + _load_all_data_cached() + if not FINANCIAL_CONCEPTS: + return {"error": "Failed to load financial concepts."} + + selected_concept = random.choice(FINANCIAL_CONCEPTS) + target_unknown = selected_concept["target_unknown"] + + all_variables_data_formatted = {} # Stores full data dicts from value_sampler, keyed by concept var name + formula_context_vars = {} # Stores raw numerical values for formula evaluation, keyed by concept var name + + # 1. Generate Known Values + for var_key in selected_concept.get("required_knowns_for_target", []): + if var_key == "m_compounding_periods_per_year": + comp_freq_data = value_sampler.get_random_compounding_frequency() + if comp_freq_data: + all_variables_data_formatted[var_key] = comp_freq_data # Stores {'name': 'monthly', 'm_value': 12, ...} + formula_context_vars[var_key] = comp_freq_data["m_value"] + else: + return {"error": f"Failed to generate compounding frequency for {selected_concept['concept_id']}"} + else: + value_range_key = CONCEPT_VAR_TO_VALUERANGE_KEY.get(var_key) + if not value_range_key: + # This might be an intermediate variable that shouldn't be generated directly + # Or a variable that doesn't directly map to a single range (e.g. time in days from dates) + # For now, assume required_knowns are directly generatable or handled specifically. + print(f"Warning: No value_range_key mapping for required known '{var_key}' in concept '{selected_concept['concept_id']}'. Skipping direct generation.") + continue + + var_data = value_sampler.get_value_for_variable(value_range_key) + if var_data: + all_variables_data_formatted[var_key] = var_data + formula_context_vars[var_key] = var_data["value"] + else: + return {"error": f"Failed to generate value for '{var_key}' (mapped from '{value_range_key}') for concept '{selected_concept['concept_id']}'"} + + # 2. Handle Time Conversions (Example for Simple Interest if n_time_years is needed but months/days are used) + # This logic can be expanded. For now, assume n_time_years is directly generated if listed as required. + # If a concept *requires* n_time_years, but we want to sometimes *start* from months or days: + if selected_concept["financial_topic"] == "Simple Interest": + if "n_time_years" in formula_context_vars and random.random() < 0.3: # 30% chance to convert from months + original_months_data = value_sampler.get_value_for_variable("time_months") + if original_months_data: + all_variables_data_formatted["n_time_months"] = original_months_data + # Override n_time_years with converted value + formula_context_vars["n_time_years"] = original_months_data["value"] / 12.0 + # Update the n_time_years entry in all_variables_data_formatted as well + all_variables_data_formatted["n_time_years"] = { + 'key': 'time_years', # original key from value_ranges + 'value': formula_context_vars["n_time_years"], + 'unit': 'years', + 'display_precision': all_variables_data_formatted["n_time_years"].get('display_precision', 4) # keep original precision setting + } + # Could add similar logic for days -> years conversion here, possibly using date_utils for exact days. + + # 3. Calculate Intermediate Variables defined in the concept's formulas + # These are formulas NOT for the target_unknown. + # Ensure they are calculated in a sensible order if there are dependencies. + # For now, assume formulas are simple enough or ordered correctly in JSON. + # A more robust way would be to build a dependency graph. + + # Create a list of formulas to evaluate, target last + formulas_to_eval = [] + if isinstance(selected_concept["formulas"], dict): + for var, formula_str in selected_concept["formulas"].items(): + if var != target_unknown: + formulas_to_eval.append((var, formula_str)) + if target_unknown in selected_concept["formulas"]: # Add target formula last + formulas_to_eval.append((target_unknown, selected_concept["formulas"][target_unknown])) + else: # Should not happen based on current JSON structure + return {"error": f"Formulas for concept {selected_concept['concept_id']} are not in expected dict format."} + + + calculated_solution_value = None + calculated_solution_data_formatted = None + + for var_to_calc, formula_str in formulas_to_eval: + calc_value = formula_evaluator.evaluate_formula(formula_str, formula_context_vars) + if calc_value is None: + return {"error": f"Failed to evaluate formula for '{var_to_calc}' in concept '{selected_concept['concept_id']}'. Context: {formula_context_vars}"} + + formula_context_vars[var_to_calc] = calc_value # Add to context for subsequent formulas + + # Create formatted data for this calculated variable (for narrative/solution) + # Need to determine its type (currency, rate, time, etc.) for proper formatting. + # We can infer from CONCEPT_VAR_TO_VALUERANGE_KEY or add 'type' to financial_concepts.json vars. + value_range_key_for_calc_var = CONCEPT_VAR_TO_VALUERANGE_KEY.get(var_to_calc) + base_config = VALUE_RANGES_CACHE.get(value_range_key_for_calc_var, {}) if VALUE_RANGES_CACHE else {} + + formatted_data_for_calc_var = { + 'key': var_to_calc, # Use the concept's variable name + 'value': calc_value, + 'currency': base_config.get('currency'), + 'unit': base_config.get('unit'), + 'unit_display': base_config.get('unit_display'), + 'display_precision': base_config.get('display_precision', base_config.get('decimals')) + } + # If it's a rate, ensure unit_display is set correctly + if "rate" in var_to_calc.lower() and not formatted_data_for_calc_var.get('unit_display'): + if value_range_key_for_calc_var and VALUE_RANGES_CACHE and value_range_key_for_calc_var in VALUE_RANGES_CACHE: + formatted_data_for_calc_var['unit_display'] = VALUE_RANGES_CACHE[value_range_key_for_calc_var].get('unit_display', '%') # Default to % + else: # Fallback if no specific range key + formatted_data_for_calc_var['unit_display'] = '%' if var_to_calc != "n_total_compounding_periods" else "periods" + + + all_variables_data_formatted[var_to_calc] = formatted_data_for_calc_var + + if var_to_calc == target_unknown: + calculated_solution_value = calc_value + calculated_solution_data_formatted = formatted_data_for_calc_var + + + if calculated_solution_value is None: + return {"error": f"Target unknown '{target_unknown}' was not calculated for concept '{selected_concept['concept_id']}'."} + + # 4. Build Narrative + problem_narrative = narrative_builder.build_narrative(selected_concept, all_variables_data_formatted, target_unknown) + + # 5. Generate Guided Solution + solution_steps = solution_presenter.generate_guided_solution(selected_concept, all_variables_data_formatted, calculated_solution_data_formatted) + + return { + "concept_id": selected_concept["concept_id"], + "topic": selected_concept["financial_topic"], + "problem_statement": problem_narrative, + "target_unknown_key": target_unknown, + "known_values_data": {k: v for k, v in all_variables_data_formatted.items() if k in selected_concept.get("required_knowns_for_target", []) or k in ["m_compounding_periods_per_year", "n_time_months", "start_date", "end_date"]}, # Show originally generated knowns + "all_variables_for_solution": all_variables_data_formatted, # Includes intermediates + "calculated_answer_raw": calculated_solution_value, + "calculated_answer_formatted": value_sampler.format_value_for_display(calculated_solution_data_formatted), + "solution_steps": solution_steps + } + + +if __name__ == '__main__': + print("Generating a sample financial problem...\n") + # Ensure data is loaded for sub-modules if they cache independently on first call + value_sampler._get_value_ranges_cached() + narrative_builder._get_names_data_cached() + narrative_builder._get_text_snippets_cached() + solution_presenter._get_text_snippets_cached() + + + for i in range(3): # Generate a few problems + problem = generate_problem() + print(f"\n--- Problem {i+1} ---") + if "error" in problem: + print(f"Error generating problem: {problem['error']}") + else: + print(f"Concept ID: {problem['concept_id']}") + print(f"Topic: {problem['topic']}") + print("\nProblem Statement:") + print(problem['problem_statement']) + + # print("\nKnown Values (Formatted for display):") + # for k, v_data in problem['known_values_data'].items(): + # print(f" {k}: {value_sampler.format_value_for_display(v_data)}") + + print(f"\nQuestion: What is the {TEXT_SNIPPETS_DATA['variable_descriptions'].get(problem['target_unknown_key'], problem['target_unknown_key'])}?") + + print("\nGuided Solution:") + for step in problem['solution_steps']: + print(step) + + print(f"\nFinal Answer ({problem['target_unknown_key']}): {problem['calculated_answer_formatted']}") + print("---------------------------------------\n") + + # Test a specific concept if needed for debugging + # For example, to test COMPOUND_INTEREST_SOLVE_FOR_TIME which uses math.log + # You might need to temporarily modify FINANCIAL_CONCEPTS to pick only this one. + # Or add a loop to find it. + # concepts = data_loader.get_financial_concepts() + # time_concept = next((c for c in concepts if c["concept_id"] == "COMPOUND_INTEREST_SOLVE_FOR_TIME"), None) + # if time_concept: + # FINANCIAL_CONCEPTS = data_loader.get_financial_concepts() # Reload to get all + + concepts_to_test = [ + "EXACT_SIMPLE_INTEREST_SOLVE_FOR_I", + "ORDINARY_SIMPLE_INTEREST_SOLVE_FOR_F" + ] + + all_concepts_loaded = data_loader.get_financial_concepts() # Ensure fresh load if not already cached by _load_all_data_cached + + for concept_id_to_test in concepts_to_test: + specific_concept = next((c for c in all_concepts_loaded if c["concept_id"] == concept_id_to_test), None) + if specific_concept: + # Temporarily override the global FINANCIAL_CONCEPTS for this specific test run + global FINANCIAL_CONCEPTS + original_financial_concepts = FINANCIAL_CONCEPTS + FINANCIAL_CONCEPTS = [specific_concept] + + print(f"\n--- Testing Specific Concept: {concept_id_to_test} ---") + problem = generate_problem() + if "error" in problem: + print(f"Error generating problem: {problem['error']}") + else: + print(f"Concept ID: {problem['concept_id']}") + print(f"Topic: {problem['topic']}") + print("\nProblem Statement:") + print(problem['problem_statement']) + + print(f"\nQuestion: What is the {TEXT_SNIPPETS_DATA['variable_descriptions'].get(problem['target_unknown_key'], problem['target_unknown_key'])}?") + + # print("\nKnown Values (Raw for debugging):") + # for k, v_data in problem['known_values_data'].items(): + # print(f" {k}: {v_data['value']} (Type: {type(v_data['value'])})") + # print("\nAll Variables for Solution (Raw for debugging):") + # for k, v_data in problem['all_variables_for_solution'].items(): + # print(f" {k}: {v_data['value']} (Type: {type(v_data['value'])})") + + + print("\nGuided Solution:") + for step in problem['solution_steps']: + print(step) + + print(f"\nFinal Answer ({problem['target_unknown_key']}): {problem['calculated_answer_formatted']}") + print("---------------------------------------\n") + + FINANCIAL_CONCEPTS = original_financial_concepts # Restore original concepts + else: + print(f"\n--- Concept {concept_id_to_test} not found for specific testing. ---") diff --git a/src/solution_presenter.py b/src/solution_presenter.py new file mode 100644 index 0000000..62336b3 --- /dev/null +++ b/src/solution_presenter.py @@ -0,0 +1,377 @@ +import random +from src.data_loader import get_text_snippets +from src.value_sampler import format_value_for_display + +# Cache loaded data +TEXT_SNIPPETS = None + +def _get_text_snippets_cached(): + global TEXT_SNIPPETS + if TEXT_SNIPPETS is None: + TEXT_SNIPPETS = get_text_snippets() + return TEXT_SNIPPETS + +def get_snippet(key_path): + """Retrieves a snippet from text_snippets.json using a dot-separated path.""" + snippets = _get_text_snippets_cached() + keys = key_path.split('.') + current_level = snippets + for key in keys: + if isinstance(current_level, dict) and key in current_level: + current_level = current_level[key] + else: + return f"{{Error: Snippet not found for {key_path}}}" + return current_level + +def format_formula_for_display(formula_str, context_vars_data, target_variable): + """ + Formats a formula string for display by substituting variable placeholders with their display values. + Example: "F = P * (1 + i * n)" with context becomes "F = Php 1,000.00 * (1 + 0.05 * 2.0 years)" + """ + # This is a simplified version. A more robust version might parse the formula + # or use regex to replace only whole variable names. + # For now, simple string replacement. + + # Sort keys by length (descending) to replace longer variable names first (e.g., "i_simple_annual" before "i") + # This is not perfect but helps in some cases. + # A better approach would be to use the actual variable names from the financial_concept. + + # We need the actual variable names used in the formula string, not necessarily the keys in context_vars_data. + # The financial_concept's formula string uses specific names. + + # For now, let's assume formula_str uses the keys from context_vars_data directly. + # This needs to align with how formulas are defined in financial_concepts.json. + + # Let's refine this: the formula string in financial_concepts.json uses specific variable names. + # The context_vars_data keys should match these. + + display_formula = formula_str + + # Replace variables with their formatted display values + # We need to be careful not to replace parts of words. + # Example: if 'P' is a variable, don't replace 'P' in 'Principal'. + # This is tricky with simple string replacement. + # A better way is to format the *values* that will be substituted into the formula string + # when it's presented as "Substitute: Formula_with_values". + + # Let's re-think. The formula itself (symbolic) should be displayed as is. + # The "substitution" step is where values are shown. + + return formula_str # For now, just return the symbolic formula. Substitution will be handled in a specific step. + +def generate_guided_solution(financial_concept, all_variables_data, calculated_solution_data): + """ + Generates a step-by-step guided solution. + + Args: + financial_concept (dict): The financial concept definition. + all_variables_data (dict): Dict of all variable data (knowns, intermediates, and the final unknown's calculated value). + e.g., {"P": {'value': 1000, ...}, "i_simple_annual": {'value': 0.05, ...}, "F_simple": {'value': 1100, ...}} + calculated_solution_data (dict): Data for the final calculated unknown variable. + + Returns: + list: A list of strings, where each string is a step in the solution. + """ + solution_steps_text = [] + step_counter = 1 + + # Get the symbolic formula for the target unknown + target_unknown_key = financial_concept["target_unknown"] + symbolic_formula_for_target = financial_concept["formulas"].get(target_unknown_key, "Formula not defined") + + # If there are multiple formulas (e.g. for intermediate steps), we might need to list them all + # or pick the primary one. For now, assume the target_unknown key in "formulas" is the main one. + + for step_key_path in financial_concept.get("solution_step_keys", []): + step_text_template = get_snippet(step_key_path) + + formatted_step_text = f"{step_counter}. {step_text_template}" + + # --- Handle specific step types for dynamic content --- + if step_key_path == "solution_guidance.identify_knowns": + knowns_list_text = [f"{step_counter}. {step_text_template}"] + sub_step_counter = 0 + for var_key in financial_concept.get("required_knowns_for_target", []): + if var_key in all_variables_data: + var_data = all_variables_data[var_key] + var_desc = get_snippet(f"variable_descriptions.{var_key}") + knowns_list_text.append(f" - {var_desc} ({var_key}): {format_value_for_display(var_data)}") + sub_step_counter +=1 + # Add other relevant knowns if not in required_knowns_for_target but present (e.g. m_compounding_periods_per_year name) + if "m_compounding_periods_per_year" in all_variables_data and "m_compounding_periods_per_year" not in financial_concept.get("required_knowns_for_target", []): + var_data = all_variables_data["m_compounding_periods_per_year"] + var_desc = get_snippet(f"variable_descriptions.m_compounding_periods_per_year") + knowns_list_text.append(f" - {var_desc} (m): {var_data['name']} (m={var_data['value']})") + + solution_steps_text.extend(knowns_list_text) + step_counter += 1 + continue # Move to next main step + + elif step_key_path == "solution_guidance.state_formula": + # Display the main formula for the target unknown + formula_to_display = symbolic_formula_for_target + if financial_concept["target_unknown"] == "i_rate_per_period" and "COMPOUND_INTEREST_SOLVE_FOR_RATE" in financial_concept["concept_id"]: # Special case for rate + formula_to_display = "i = (F/P)^(1/n) - 1, then r = i * m" # More user friendly + elif financial_concept["target_unknown"] == "n_total_compounding_periods" and "COMPOUND_INTEREST_SOLVE_FOR_TIME" in financial_concept["concept_id"]: + formula_to_display = "n = log(F/P) / log(1+i), then t = n / m" + # For Exact/Ordinary interest, the formula might involve n_time_years_fractional + elif "n_time_years_fractional" in symbolic_formula_for_target: + # The formula in financial_concepts.json already uses n_time_years_fractional + pass # formula_to_display is already correct + + formatted_step_text = formatted_step_text.replace("{formula_symbolic}", formula_to_display) + + elif step_key_path == "solution_guidance.substitute_values": + subst_formula_str = symbolic_formula_for_target + sub_values_text = [f"{step_counter}. {step_text_template.replace('{formula_symbolic}', symbolic_formula_for_target)}"] + solution_steps_text.extend(sub_values_text) + step_counter +=1 + continue + + elif step_key_path == "solution_guidance.perform_calculation": + pass + + elif step_key_path == "solution_guidance.final_answer_is": + unknown_desc = get_snippet(f"variable_descriptions.{target_unknown_key}") + formatted_step_text = formatted_step_text.replace("{unknown_variable_description}", unknown_desc) + formatted_step_text += f" {format_value_for_display(calculated_solution_data)}" + + elif step_key_path == "solution_guidance.convert_time_to_years": + original_time_var = None + original_time_unit = "" + if "n_time_months" in all_variables_data and "n_time_years" in all_variables_data: + original_time_var = all_variables_data["n_time_months"] + original_time_unit = "months" + elif "n_time_days" in all_variables_data and "n_time_years" in all_variables_data and \ + "start_date" not in all_variables_data: # Ensure not Exact/Ordinary context + original_time_var = all_variables_data["n_time_days"] + original_time_unit = "days" + + if original_time_var and "n_time_years" in all_variables_data: + formatted_step_text = formatted_step_text.format( + original_time_value=original_time_var['value'], + original_time_unit=original_time_unit, + converted_time_value_years=f"{all_variables_data['n_time_years']['value']:.4f}" + ) + else: + if (financial_concept["financial_topic"] == "Simple Interest" and \ + "n_time_years" in financial_concept.get("required_knowns_for_target", [])) or \ + (financial_concept["financial_topic"] in ["Exact Simple Interest", "Ordinary Simple Interest"]): + continue + formatted_step_text = f"{step_counter}. (Time conversion step - data missing or not applicable)" + + elif step_key_path == "solution_guidance.calculate_interest_rate_per_period": + if "r_nominal_annual" in all_variables_data and \ + "m_compounding_periods_per_year" in all_variables_data and \ + "i_rate_per_period" in all_variables_data: + formatted_step_text = formatted_step_text.format( + nominal_rate_decimal=f"{all_variables_data['r_nominal_annual']['value']:.4f}", + compounding_periods_per_year=all_variables_data['m_compounding_periods_per_year']['value'], + interest_rate_per_period_decimal=f"{all_variables_data['i_rate_per_period']['value']:.6f}" + ) + else: continue + + elif step_key_path == "solution_guidance.calculate_total_periods": + if "t_years" in all_variables_data and \ + "m_compounding_periods_per_year" in all_variables_data and \ + "n_total_compounding_periods" in all_variables_data: + formatted_step_text = formatted_step_text.format( + time_in_years=all_variables_data['t_years']['value'], + compounding_periods_per_year=all_variables_data['m_compounding_periods_per_year']['value'], + total_periods=all_variables_data['n_total_compounding_periods']['value'] + ) + else: continue + + elif step_key_path == "solution_guidance.days_in_period": + if "start_date" in all_variables_data and \ + "end_date" in all_variables_data and \ + "n_time_days" in all_variables_data: + formatted_step_text = formatted_step_text.format( + start_date=format_value_for_display(all_variables_data['start_date']), + end_date=format_value_for_display(all_variables_data['end_date']), + number_of_days=all_variables_data['n_time_days']['value'] + ) + else: continue + + elif step_key_path == "solution_guidance.check_leap_year": + year_to_check_data = all_variables_data.get("time_base_year_for_exact") + if not year_to_check_data and "start_date" in all_variables_data: + year_to_check_data = {'value': all_variables_data["start_date"]['value'].year} + + if year_to_check_data: + from src.date_utils import is_leap_year + year_val = year_to_check_data['value'] + is_leap = is_leap_year(year_val) + formatted_step_text = formatted_step_text.format( + year=year_val, + is_or_is_not="is" if is_leap else "is not" + ) + else: continue + + elif step_key_path == "solution_guidance.determine_time_base_exact": + year_for_base_data = all_variables_data.get("time_base_year_for_exact") + days_in_year_data = all_variables_data.get("time_base_days") + + if year_for_base_data and days_in_year_data: + formatted_step_text = formatted_step_text.format( + year=year_for_base_data['value'], + days_in_year=days_in_year_data['value'] + ) + else: continue + + elif step_key_path == "solution_guidance.determine_time_base_ordinary": + if "time_base_days" in all_variables_data and all_variables_data["time_base_days"]['value'] == 360: + pass + else: continue + + elif step_key_path == "solution_guidance.calculate_n_time_years_fractional": + if "n_time_days" in all_variables_data and \ + "time_base_days" in all_variables_data and \ + "n_time_years_fractional" in all_variables_data: + formatted_step_text = formatted_step_text.format( + n_time_days=all_variables_data['n_time_days']['value'], + time_base_days=all_variables_data['time_base_days']['value'], + n_time_years_fractional_value=f"{all_variables_data['n_time_years_fractional']['value']:.6f}" + ) + else: continue + + elif step_key_path == "solution_guidance.intermediate_step": + if "Db_discount_amount" in all_variables_data and "BANKERS_DISCOUNT" in financial_concept["concept_id"]: + intermediate_var_name = "Db_discount_amount" + intermediate_var_desc = get_snippet(f"variable_descriptions.{intermediate_var_name}") + formatted_step_text = formatted_step_text.format(step_name=intermediate_var_desc) + f" {format_value_for_display(all_variables_data[intermediate_var_name])}" + elif "i_rate_per_period" in all_variables_data and "COMPOUND_INTEREST_SOLVE_FOR_RATE" in financial_concept["concept_id"] and target_unknown_key == "r_nominal_annual": + intermediate_var_name = "i_rate_per_period" + intermediate_var_desc = get_snippet(f"variable_descriptions.{intermediate_var_name}") + formatted_step_text = formatted_step_text.format(step_name=intermediate_var_desc) + f" {all_variables_data[intermediate_var_name]['value']:.6f}" + elif "n_total_compounding_periods" in all_variables_data and "COMPOUND_INTEREST_SOLVE_FOR_TIME" in financial_concept["concept_id"] and target_unknown_key == "t_years": + intermediate_var_name = "n_total_compounding_periods" + intermediate_var_desc = get_snippet(f"variable_descriptions.{intermediate_var_name}") + formatted_step_text = formatted_step_text.format(step_name=intermediate_var_desc) + f" {all_variables_data[intermediate_var_name]['value']:.4f} periods" + # For Exact/Ordinary interest, n_time_years_fractional is an intermediate step before the main formula + elif "n_time_years_fractional" in all_variables_data and \ + financial_concept["financial_topic"] in ["Exact Simple Interest", "Ordinary Simple Interest"] and \ + step_key_path == "solution_guidance.intermediate_step": # This condition might be too generic + # The specific step "calculate_n_time_years_fractional" should handle this. + # If "intermediate_step" is used generically for this, it needs better targeting. + # For now, assume "calculate_n_time_years_fractional" is the explicit step. + # This generic "intermediate_step" might not be needed for these concepts if steps are explicit. + continue # Let specific handlers deal with it. + + else: + formatted_step_text = f"{step_counter}. (Intermediate calculation step)" + + solution_steps_text.append(formatted_step_text) + step_counter += 1 + + return solution_steps_text + +if __name__ == '__main__': + print("Testing Solution Presenter:") + from datetime import date + + # Mock data for testing + mock_snippets_data = { + "solution_guidance": { + "identify_knowns": "First, identify knowns:", + "state_formula": "The formula is: {formula_symbolic}", + "substitute_values": "Substitute values into: {formula_symbolic}", + "perform_calculation": "Performing calculation...", + "final_answer_is": "Therefore, the {unknown_variable_description} is:", + "convert_time_to_years": "Convert time: {original_time_value} {original_time_unit} = {converted_time_value_years} years.", + "calculate_interest_rate_per_period": "i = r/m = {nominal_rate_decimal}/{compounding_periods_per_year} = {interest_rate_per_period_decimal}.", + "calculate_total_periods": "n = t*m = {time_in_years}*{compounding_periods_per_year} = {total_periods} periods.", + "intermediate_step": "Intermediate {step_name}:", + "days_in_period": "Days from {start_date} to {end_date}: {number_of_days} days.", + "check_leap_year": "{year} is {is_or_is_not} a leap year.", + "determine_time_base_exact": "Exact time base for {year}: {days_in_year} days.", + "determine_time_base_ordinary": "Ordinary time base: 360 days.", + "calculate_n_time_years_fractional": "t_fractional = {n_time_days}/{time_base_days} = {n_time_years_fractional_value}." + }, + "variable_descriptions": { + "P": "Principal", "F_simple": "Future Value (Simple)", "i_simple_annual": "Annual Simple Interest Rate", + "n_time_years": "Time in Years", "n_time_months": "Time in Months", "n_time_days": "Time in Days", + "F_compound": "Future Value (Compound)", "r_nominal_annual": "Nominal Annual Rate", + "m_compounding_periods_per_year": "Compounding Frequency", "t_years": "Time in Years (Compound)", + "i_rate_per_period": "Interest Rate per Period", "n_total_compounding_periods": "Total Compounding Periods", + "Db_discount_amount": "Banker's Discount Amount", + "start_date": "Start Date", "end_date": "End Date", "time_base_days": "Time Base (Days)", + "n_time_years_fractional": "Time (Fractional Years)", + "I_exact_simple": "Exact Simple Interest", "F_exact_simple": "Future Value (Exact Simple Interest)", + "I_ordinary_simple": "Ordinary Simple Interest", "F_ordinary_simple": "Future Value (Ordinary Simple Interest)" + } + } + # Override loader for testing + _get_text_snippets_cached = lambda: mock_snippets_data + + # Test Case 4: Exact Simple Interest, Solve for I + concept4 = { + "concept_id": "EXACT_SIMPLE_INTEREST_SOLVE_FOR_I", + "financial_topic": "Exact Simple Interest", + "target_unknown": "I_exact_simple", + "formulas": { + "n_time_years_fractional": "n_time_days / time_base_days", + "I_exact_simple": "P * i_simple_annual * n_time_years_fractional" + }, + "required_knowns_for_target": ["P", "i_simple_annual", "start_date", "end_date"], + "solution_step_keys": [ + "solution_guidance.identify_knowns", + "solution_guidance.days_in_period", + "solution_guidance.check_leap_year", # Refers to time_base_year_for_exact + "solution_guidance.determine_time_base_exact", + "solution_guidance.calculate_n_time_years_fractional", + "solution_guidance.state_formula", + "solution_guidance.substitute_values", + "solution_guidance.perform_calculation", + "solution_guidance.final_answer_is" + ] + } + all_vars4 = { + "P": {'key': 'P', 'value': 5000.0, 'currency': 'Php', 'display_precision': 2}, + "i_simple_annual": {'key': 'i_simple_annual', 'value': 0.05, 'unit_display': '% p.a.', 'display_precision': 2}, + "start_date": {'key': 'start_date', 'value': date(2023, 1, 15), 'unit': 'date'}, + "end_date": {'key': 'end_date', 'value': date(2023, 7, 15), 'unit': 'date'}, + "n_time_days": {'key': 'n_time_days', 'value': 181, 'unit': 'days', 'display_precision': 0}, # Calculated by date_utils + "time_base_year_for_exact": {'key': 'time_base_year_for_exact', 'value': 2023}, # Set by problem_engine + "time_base_days": {'key': 'time_base_days', 'value': 365, 'unit': 'days', 'display_precision': 0}, # Calculated by problem_engine + "n_time_years_fractional": {'key': 'n_time_years_fractional', 'value': 181/365, 'display_precision': 6}, # Calculated + "I_exact_simple": {'key': 'I_exact_simple', 'value': 5000 * 0.05 * (181/365), 'currency': 'Php', 'display_precision': 2} + } + solution4_text = generate_guided_solution(concept4, all_vars4, all_vars4["I_exact_simple"]) + print("\n--- Test Case 4: Exact Simple Interest (I) ---") + for step in solution4_text: print(step) + + # Test Case 5: Ordinary Simple Interest, Solve for F + concept5 = { + "concept_id": "ORDINARY_SIMPLE_INTEREST_SOLVE_FOR_F", + "financial_topic": "Ordinary Simple Interest", + "target_unknown": "F_ordinary_simple", + "formulas": { + "n_time_years_fractional": "n_time_days / time_base_days", + "F_ordinary_simple": "P * (1 + i_simple_annual * n_time_years_fractional)" + }, + "required_knowns_for_target": ["P", "i_simple_annual", "start_date", "end_date"], + "solution_step_keys": [ + "solution_guidance.identify_knowns", + "solution_guidance.days_in_period", + "solution_guidance.determine_time_base_ordinary", # No check_leap_year for ordinary + "solution_guidance.calculate_n_time_years_fractional", + "solution_guidance.state_formula", + "solution_guidance.substitute_values", + "solution_guidance.perform_calculation", + "solution_guidance.final_answer_is" + ] + } + all_vars5 = { + "P": {'key': 'P', 'value': 10000.0, 'currency': 'Php', 'display_precision': 2}, + "i_simple_annual": {'key': 'i_simple_annual', 'value': 0.12, 'unit_display': '% p.a.', 'display_precision': 2}, + "start_date": {'key': 'start_date', 'value': date(2024, 3, 1), 'unit': 'date'}, # 2024 is a leap year + "end_date": {'key': 'end_date', 'value': date(2024, 6, 29), 'unit': 'date'}, # 120 days + "n_time_days": {'key': 'n_time_days', 'value': 120, 'unit': 'days', 'display_precision': 0}, + "time_base_days": {'key': 'time_base_days', 'value': 360, 'unit': 'days', 'display_precision': 0}, + "n_time_years_fractional": {'key': 'n_time_years_fractional', 'value': 120/360, 'display_precision': 6}, + "F_ordinary_simple": {'key': 'F_ordinary_simple', 'value': 10000 * (1 + 0.12 * (120/360)), 'currency': 'Php', 'display_precision': 2} + } + solution5_text = generate_guided_solution(concept5, all_vars5, all_vars5["F_ordinary_simple"]) + print("\n--- Test Case 5: Ordinary Simple Interest (F) ---") + for step in solution5_text: print(step) diff --git a/src/value_sampler.py b/src/value_sampler.py new file mode 100644 index 0000000..36f0463 --- /dev/null +++ b/src/value_sampler.py @@ -0,0 +1,159 @@ +import random +import decimal +from src.data_loader import get_value_ranges + +# Cache loaded data to avoid repeated file I/O +VALUE_RANGES = None + +def _get_value_ranges_cached(): + """Returns cached value_ranges data, loading if not already cached.""" + global VALUE_RANGES + if VALUE_RANGES is None: + VALUE_RANGES = get_value_ranges() + return VALUE_RANGES + +def get_random_float(min_val, max_val, precision=None): + """Generates a random float between min_val and max_val with specified precision.""" + val = random.uniform(min_val, max_val) + if precision is not None: + # Using Decimal for precise rounding + return float(decimal.Decimal(str(val)).quantize(decimal.Decimal('1e-' + str(precision)), rounding=decimal.ROUND_HALF_UP)) + return val + +def get_random_int(min_val, max_val): + """Generates a random integer between min_val and max_val (inclusive).""" + return random.randint(min_val, max_val) + +def get_value_for_variable(variable_key): + """ + Generates a random value for a given variable key based on value_ranges.json. + Returns a dictionary containing the value and its unit/currency if applicable. + Example: {'value': 15000.50, 'currency': 'Php', 'display_str': 'Php 15,000.50'} + {'value': 0.12, 'unit_display': '% per annum', 'display_str': '12.00% per annum'} + {'value': 5, 'unit': 'years', 'display_str': '5 years'} + """ + ranges = _get_value_ranges_cached() + if not ranges or variable_key not in ranges: + print(f"Error: No range definition found for variable key '{variable_key}' in value_ranges.json") + return None + + config = ranges[variable_key] + val = None + result = {'key': variable_key} + + if config.get("integer", False): + val = get_random_int(config["min"], config["max"]) + result['value'] = val + else: + # For floats, use internal_precision for generation if available, otherwise default to a reasonable precision + # Display precision will be handled separately or by a formatting function + internal_precision = config.get("internal_precision", config.get("decimals", 4)) # Default to 4 if no precision specified + val = get_random_float(config["min"], config["max"], internal_precision) + result['value'] = val + + if "currency" in config: + result["currency"] = config["currency"] + if "unit" in config: + result["unit"] = config["unit"] + if "unit_display" in config: # For rates like "% per annum" + result["unit_display"] = config["unit_display"] + + # Store precision details for later formatting + result["display_precision"] = config.get("display_precision", config.get("decimals")) # Decimals is often used for currency + + return result + +def get_random_compounding_frequency(): + """Selects a random compounding frequency and its 'm' value.""" + ranges = _get_value_ranges_cached() + if not ranges or "compounding_frequency_options" not in ranges: + print("Error: 'compounding_frequency_options' not found in value_ranges.json") + return None, None + + options = ranges["compounding_frequency_options"] + frequency_name = random.choice(list(options.keys())) + m_value = options[frequency_name] + return {"name": frequency_name, "m_value": m_value, "key": "m_compounding_periods_per_year", "value": m_value} + + +def format_value_for_display(value_data): + """ + Formats a value generated by get_value_for_variable for display. + Example input: {'key': 'principal', 'value': 15000.5, 'currency': 'Php', 'display_precision': 2} + Example output: "Php 15,000.50" + Example input: {'key': 'simple_interest_rate_annual', 'value': 0.12, 'unit_display': '% per annum', 'display_precision': 2} + Example output: "12.00% per annum" + """ + if not isinstance(value_data, dict) or 'value' not in value_data: + return str(value_data) # Fallback + + val = value_data['value'] + display_precision = value_data.get('display_precision') + + # Format number with precision + if display_precision is not None and isinstance(val, (float, decimal.Decimal)): + # Check if it's a rate to be displayed as percentage + if "unit_display" in value_data and "%" in value_data["unit_display"]: + formatted_num = f"{val * 100:.{display_precision}f}" + else: + formatted_num = f"{val:.{display_precision}f}" + # Add thousands separator for currency-like values + if "currency" in value_data and val >= 1000: + parts = formatted_num.split('.') + parts[0] = "{:,}".format(int(parts[0].replace(',', ''))) # Apply to integer part + formatted_num = ".".join(parts) + + elif isinstance(val, int): + formatted_num = str(val) + if "currency" in value_data and val >= 1000: + formatted_num = "{:,}".format(val) + else: + formatted_num = str(val) + + + if "currency" in value_data: + return f"{value_data['currency']} {formatted_num}" + elif "unit_display" in value_data: # e.g., "% per annum" + return f"{formatted_num}{value_data['unit_display']}" # Assumes % is part of unit_display or handled by *100 + elif "unit" in value_data: # e.g., "years" + return f"{formatted_num} {value_data['unit']}" + else: + return formatted_num + + +if __name__ == '__main__': + print("Testing Value Sampler:") + + # Test get_value_for_variable + print("\n--- Testing get_value_for_variable ---") + principal_data = get_value_for_variable("principal") + if principal_data: + print(f"Generated Principal Data: {principal_data}") + print(f"Formatted Principal: {format_value_for_display(principal_data)}") + + rate_data = get_value_for_variable("simple_interest_rate_annual") + if rate_data: + print(f"Generated Rate Data: {rate_data}") + print(f"Formatted Rate: {format_value_for_display(rate_data)}") + + time_data = get_value_for_variable("time_years") + if time_data: + print(f"Generated Time Data: {time_data}") + print(f"Formatted Time: {format_value_for_display(time_data)}") + + # Test get_random_compounding_frequency + print("\n--- Testing get_random_compounding_frequency ---") + comp_freq = get_random_compounding_frequency() + if comp_freq: + print(f"Generated Compounding Frequency: {comp_freq}") + + # Test edge cases or specific formatting + print("\n--- Testing Specific Formatting ---") + test_currency_large = {'key': 'principal', 'value': 1234567.89, 'currency': 'Php', 'display_precision': 2} + print(f"Large Currency: {format_value_for_display(test_currency_large)}") + test_currency_small = {'key': 'principal', 'value': 123.45, 'currency': 'Php', 'display_precision': 2} + print(f"Small Currency: {format_value_for_display(test_currency_small)}") + test_rate_percent = {'key': 'simple_interest_rate_annual', 'value': 0.085, 'unit_display': '% per annum', 'display_precision': 2} + print(f"Rate as Percent: {format_value_for_display(test_rate_percent)}") + test_integer_unit = {'key': 'time_years', 'value': 5, 'unit': 'years'} + print(f"Integer with Unit: {format_value_for_display(test_integer_unit)}")