Compare commits
2 Commits
bb8e91b9d8
...
f02f848ada
| Author | SHA1 | Date | |
|---|---|---|---|
| f02f848ada | |||
| f1d18ec989 |
1
.python-version
Normal file
1
.python-version
Normal file
@ -0,0 +1 @@
|
||||
3.9
|
||||
243
README.md
243
README.md
@ -1,2 +1,243 @@
|
||||
# problem-generator
|
||||
# Engineering Economy Problem Generator
|
||||
|
||||
This project is a Python-based system designed to procedurally generate engineering economy problems. It focuses on topics such as simple and compound interest, effective interest rates, continuous compounding, Banker's Discount, and date-specific interest calculations (exact and ordinary). The system aims to create varied and realistic word problems with step-by-step solutions, suitable for practice and learning.
|
||||
|
||||
## Covered Concepts
|
||||
|
||||
The generator currently supports problems related to the following topics (aligned with typical Engineering Economy curricula):
|
||||
|
||||
* **Simple Interest:**
|
||||
* Calculating Future Value (F)
|
||||
* Calculating Present Value (P) from Future Value
|
||||
* Calculating Simple Interest Amount (I)
|
||||
* Calculating Present Value (P) from Interest Amount
|
||||
* Calculating Simple Interest Rate (i)
|
||||
* Calculating Time Period (n)
|
||||
* **Types of Simple Interest:**
|
||||
* Exact Simple Interest (using actual days in month/year, including leap year considerations)
|
||||
* Ordinary Simple Interest (using 30-day months, 360-day year)
|
||||
* **Banker's Discount:**
|
||||
* Calculating Proceeds
|
||||
* Calculating Discount Rate
|
||||
* Calculating Equivalent Simple Interest Rate
|
||||
* **Compound Interest:**
|
||||
* Calculating Future Value (F)
|
||||
* Calculating Present Value (P)
|
||||
* Calculating Nominal Interest Rate (r)
|
||||
* Calculating Time Period (t or n)
|
||||
* **Effective Rate of Interest:**
|
||||
* Calculating Effective Rate (ER) given nominal rate and compounding frequency.
|
||||
* **Continuous Compounding Interest:**
|
||||
* Calculating Future Value (F)
|
||||
* Calculating Present Value (P)
|
||||
* Calculating Nominal Interest Rate (r)
|
||||
* Calculating Time Period (t)
|
||||
* Calculating Equivalent Simple Interest Rate for a continuously compounded rate.
|
||||
|
||||
## System Architecture
|
||||
|
||||
The problem generator is a modular system composed of several Python scripts and JSON data files that work together to produce financial problems.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[problem_engine.py] --> B(data_loader.py);
|
||||
A --> C(value_sampler.py);
|
||||
A --> D(date_utils.py);
|
||||
A --> E(formula_evaluator.py);
|
||||
A --> F(narrative_builder.py);
|
||||
A --> G(solution_presenter.py);
|
||||
|
||||
B -- Reads & Caches --> H[financial_concepts.json];
|
||||
B -- Reads & Caches --> I[value_ranges.json];
|
||||
B -- Reads & Caches --> J[text_snippets.json];
|
||||
B -- Reads & Caches --> K[data/names.json];
|
||||
|
||||
C -- Uses Constraints From --> I;
|
||||
C -- Uses Date Functions From --> D;
|
||||
F -- Uses Actor Names From --> K;
|
||||
F -- Uses Text Templates From --> J;
|
||||
G -- Uses Text Templates From --> J;
|
||||
|
||||
subgraph Python Modules
|
||||
A
|
||||
B
|
||||
C
|
||||
D
|
||||
E
|
||||
F
|
||||
G
|
||||
end
|
||||
|
||||
subgraph Data Files (JSON)
|
||||
H[building_blocks/financial_concepts.json]
|
||||
I[data/value_ranges.json]
|
||||
J[data/text_snippets.json]
|
||||
K[data/names.json]
|
||||
end
|
||||
```
|
||||
|
||||
### Python Modules:
|
||||
|
||||
* **`main.py`:**
|
||||
* The main entry point for running the problem generator.
|
||||
* Handles initialization and calls the problem generation engine to produce and display problems.
|
||||
* **`problem_engine.py`:**
|
||||
* The central coordinator of the problem generation process.
|
||||
* Selects a financial concept, manages the flow of data between other modules (sampling values, evaluating formulas, building narrative, presenting solution), and assembles the final problem output.
|
||||
* Includes logic for handling special variable types (like dates) and basic plausibility checks (e.g., for interest rate calculations).
|
||||
* Caches loaded data to improve performance.
|
||||
* **`data_loader.py`:**
|
||||
* Responsible for loading data from the various JSON configuration files (`financial_concepts.json`, `value_ranges.json`, `text_snippets.json`, `names.json`).
|
||||
* Provides functions to access cached versions of this data.
|
||||
* **`value_sampler.py`:**
|
||||
* Generates random numerical values for the variables in a problem (e.g., principal, interest rate, time).
|
||||
* Uses constraints (min, max, precision, type) defined in `data/value_ranges.json`.
|
||||
* Includes functions to format these values for display (e.g., adding currency symbols, percentage signs, thousands separators, formatting dates).
|
||||
* **`date_utils.py`:**
|
||||
* Provides utility functions for date-related calculations, crucial for problems involving specific time periods (e.g., exact simple interest).
|
||||
* Includes functions for leap year checks, days in month/year, generating random dates and date periods, and calculating exact days between dates.
|
||||
* **`formula_evaluator.py`:**
|
||||
* Safely evaluates mathematical formula strings (defined in `building_blocks/financial_concepts.json`) using a restricted Python `eval()` environment.
|
||||
* Takes a formula string and a context dictionary of variables and their values, then returns the calculated result.
|
||||
* **`narrative_builder.py`:**
|
||||
* Constructs the natural language word problem.
|
||||
* Uses templates from `data/text_snippets.json` and actor names/details from `data/names.json`.
|
||||
* Combines these with the sampled numerical values (formatted by `value_sampler.py`) to create a coherent and varied problem statement.
|
||||
* **`solution_presenter.py`:**
|
||||
* Generates a step-by-step guided solution for the problem.
|
||||
* Uses `solution_step_keys` defined in `building_blocks/financial_concepts.json` to fetch corresponding solution step templates from `data/text_snippets.json`.
|
||||
* Populates these templates with the specific values and intermediate calculations for the current problem, including a step showing the formula with values substituted.
|
||||
|
||||
### Data Files (JSON):
|
||||
|
||||
* **`building_blocks/financial_concepts.json`:**
|
||||
* The core data file defining each type of financial problem the system can generate.
|
||||
* Each "concept" includes:
|
||||
* `concept_id`: A unique identifier.
|
||||
* `description`: Human-readable explanation.
|
||||
* `financial_topic`: The broader category (e.g., "Simple Interest").
|
||||
* `target_unknown`: The variable the problem will ask to solve for.
|
||||
* `variables_involved`: All variables relevant to the concept.
|
||||
* `formulas`: A dictionary of formula strings (including for intermediate variables).
|
||||
* `required_knowns_for_target`: Input variables needed to solve the problem.
|
||||
* `narrative_hooks`: Keywords to guide narrative construction.
|
||||
* `solution_step_keys`: An ordered list of keys mapping to solution step templates in `data/text_snippets.json`.
|
||||
* **`data/value_ranges.json`:**
|
||||
* Defines the constraints for generating random numerical values (min, max, currency, units, precision, type like integer/float).
|
||||
* Includes options for compounding frequencies and date generation parameters (like `date_period_generation`).
|
||||
* **`data/text_snippets.json`:**
|
||||
* A rich repository of text fragments used by `narrative_builder.py` and `solution_presenter.py`.
|
||||
* Contains templates for:
|
||||
* Actor descriptions (persons, companies).
|
||||
* Actions (loan, investment, repayment verbs).
|
||||
* Time phrases, rate phrases, compounding phrases.
|
||||
* Question starters and scenario framing elements.
|
||||
* Detailed templates for each step of the guided solution (e.g., identifying knowns, stating formula, substituting values).
|
||||
* Human-readable descriptions for variable keys.
|
||||
* **`data/names.json`:**
|
||||
* Provides lists of names (titles, first names, last names for persons) and company name components (prefixes, suffixes, industry types).
|
||||
* Also includes lists of items for loan/investment purposes to add flavor to narratives.
|
||||
|
||||
## Problem Generation Flow
|
||||
|
||||
1. **Initialization:** `problem_engine.py` (called by `main.py`) loads and caches all necessary data from JSON files via `data_loader.py`.
|
||||
2. **Concept Selection:** A financial concept is randomly chosen from `building_blocks/financial_concepts.json`.
|
||||
3. **Value Sampling:**
|
||||
* For each `required_knowns_for_target` in the selected concept, `problem_engine.py` instructs `value_sampler.py` to generate a value.
|
||||
* `value_sampler.py` uses `data/value_ranges.json` for constraints.
|
||||
* For date-related concepts (Exact/Ordinary Simple Interest), `problem_engine.py` uses `date_utils.py` to generate start/end dates and calculate the number of days, time base, etc.
|
||||
* Compounding frequencies are also sampled specifically.
|
||||
* Generated values are stored along with their metadata (units, precision).
|
||||
* Basic plausibility checks (e.g., ensuring F > P when solving for rate) are performed, with resampling attempts if needed.
|
||||
4. **Formula Evaluation (Intermediate & Final):**
|
||||
* `problem_engine.py` identifies all formulas (intermediate and target) for the concept.
|
||||
* `formula_evaluator.py` evaluates these formulas sequentially, using the sampled known values and any previously calculated intermediate values.
|
||||
5. **Narrative Construction:**
|
||||
* `narrative_builder.py` takes the selected concept, the (formatted) known values, and the target unknown variable.
|
||||
* It uses templates from `data/text_snippets.json` and names from `data/names.json` to construct a word problem.
|
||||
6. **Solution Generation:**
|
||||
* `solution_presenter.py` uses the `solution_step_keys` from the concept and corresponding templates from `data/text_snippets.json`.
|
||||
* It populates these templates with the actual problem data (knowns, intermediates, calculated answer) to create a step-by-step solution, including showing the formula with values substituted.
|
||||
7. **Output:** `problem_engine.py` returns a structured dictionary containing the problem statement, the question, all variable data, the calculated answer (raw and formatted), and the list of solution steps. `main.py` then prints this information.
|
||||
|
||||
## Key Features
|
||||
|
||||
* **Data-Driven Design:** Problem types, numerical ranges, and language are defined in external JSON files, enhancing flexibility.
|
||||
* **Modularity:** Components for value sampling, formula evaluation, narrative building, and solution presentation are distinct.
|
||||
* **Procedural Generation:** Ensures variety in generated problems.
|
||||
* **Natural Language Output:** Aims for human-readable problem statements and solutions.
|
||||
* **Step-by-Step Solutions:** Provides detailed, guided solutions, including formula substitution.
|
||||
* **Precision Control:** Manages internal precision for calculations and display precision for output.
|
||||
* **Date Handling:** Robust utilities for date calculations for Exact and Ordinary Simple Interest.
|
||||
* **Plausibility Checks:** Basic checks to avoid some unrealistic problem scenarios (e.g., large negative rates).
|
||||
|
||||
## Setup and Usage
|
||||
|
||||
This project uses `uv` for Python environment and dependency management, as indicated by `uv.lock` and `.python-version`.
|
||||
|
||||
1. **Environment Setup (Recommended):**
|
||||
* Ensure `uv` is installed.
|
||||
* Navigate to the project root directory.
|
||||
* Run `uv sync` (if `pyproject.toml` lists dependencies) or `uv pip install -r requirements.txt` (if a `requirements.txt` file exists or is generated). Refer to `pyproject.toml` for actual dependencies.
|
||||
2. **Running the Generator:**
|
||||
* The primary way to generate a problem is by running `main.py` from the project root directory:
|
||||
```bash
|
||||
python3 main.py
|
||||
```
|
||||
* The `src/problem_engine.py` script can also be run directly (e.g., `python3 src/problem_engine.py`). Its `if __name__ == '__main__':` block is currently configured to test all available concepts.
|
||||
|
||||
## Extensibility
|
||||
|
||||
The system is designed to be extensible for adding new problem types that fit the current architectural pattern (solving for a single target unknown via a primary formula, possibly with intermediate calculations).
|
||||
|
||||
* **Adding New Financial Concepts (Similar Architecture):**
|
||||
1. **Define Concept:** Add a new JSON object to `building_blocks/financial_concepts.json`. Specify:
|
||||
* `concept_id` (unique string)
|
||||
* `description` (string)
|
||||
* `financial_topic` (string, e.g., "Simple Annuity")
|
||||
* `target_unknown` (string, e.g., "F_annuity")
|
||||
* `variables_involved` (list of strings, all variables in the formulas)
|
||||
* `formulas` (dictionary: `{"variable_to_calc": "formula_string", ...}`). Ensure formulas for intermediate variables are listed before those that depend on them.
|
||||
* `required_knowns_for_target` (list of strings, input variables to be sampled)
|
||||
* `narrative_hooks` (list of strings, keywords for `narrative_builder.py`)
|
||||
* `solution_step_keys` (list of strings, keys from `text_snippets.json -> solution_guidance`)
|
||||
2. **Value Ranges:** If new variables require specific generation rules, add entries to `data/value_ranges.json`.
|
||||
3. **Text Snippets:**
|
||||
* Add descriptions for any new variables to `data/text_snippets.json` under `variable_descriptions`.
|
||||
* Add new solution step templates to `solution_guidance` if existing ones are insufficient, and use these new keys in your concept's `solution_step_keys`.
|
||||
* Add new narrative phrase templates if needed for unique storytelling.
|
||||
4. **Variable Mapping:** Update `CONCEPT_VAR_TO_VALUERANGE_KEY` in `src/problem_engine.py` if new variables need mapping to `value_ranges.json` keys for sampling.
|
||||
5. **Special Handling (If Any):** If the new concept requires unique logic in `problem_engine.py` (e.g., special date handling, unique plausibility checks) or `solution_presenter.py` (unique formatting for a solution step), those modules would need to be extended.
|
||||
* **Adding New Names/Items:** Edit `data/names.json` to add more variety to actors or scenario items.
|
||||
* **Adding New Text Snippets:** Edit `data/text_snippets.json` to add more phrasing options for narratives or solution steps.
|
||||
* **Modifying Value Ranges:** Adjust `data/value_ranges.json` to change the scope of numerical values generated.
|
||||
|
||||
### Future Enhancements / Scope for Advanced Concepts
|
||||
|
||||
The current architecture is well-suited for problems that can be solved by evaluating one or more direct formulas to find a target unknown. More complex topics from engineering economy, such as:
|
||||
|
||||
* **Equation of Value / Discrete Payments (with algebraic unknowns):** Problems like the "Acosta Holdings Loan" or "Mr. Cruz's Car Purchase" from the reference notes, where an unknown payment (X) appears in multiple terms of an equation of value and needs to be solved for algebraically.
|
||||
* **Arithmetic/Geometric Gradients:** While the "Restaurant Lease - Arithmetic Gradient" problem involves standard formulas (P/A and P/G), integrating these as a distinct, generatable concept requires defining them with their specific variables (A1, G, N) and solution steps.
|
||||
* **Annuities (Ordinary, Due, Deferred, Perpetuities):** These have their own sets of formulas and common problem structures.
|
||||
* **More Advanced Topics:** Depreciation, Bonds, Capital Budgeting (NPV, IRR), etc.
|
||||
|
||||
Implementing these advanced topics would require significant enhancements:
|
||||
|
||||
1. **Equation Solving:** The current `formula_evaluator.py` evaluates expressions but doesn't solve equations algebraically. This would likely require integrating a symbolic math library (like SymPy) or developing custom logic to set up and solve equations of value.
|
||||
2. **Complex Concept Definitions:** `building_blocks/financial_concepts.json` would need a more sophisticated structure to define multiple cash flows, their timings, relationships (e.g., payment2 = 1.5 * payment1), and the setup of an equation rather than just direct formulas for a target.
|
||||
3. **Enhanced `problem_engine.py`:** Logic to manage multiple cash flows, select focal dates, and coordinate the setup of equations for the solver.
|
||||
4. **Advanced `solution_presenter.py`:** New solution step templates and logic to explain:
|
||||
* Drawing cash flow diagrams (descriptively).
|
||||
* Choosing a focal date.
|
||||
* Bringing each cash flow to the focal date.
|
||||
* Setting up the equation of value.
|
||||
* Showing algebraic manipulation to solve for the unknown.
|
||||
* Specific steps for gradient series components.
|
||||
5. **Richer `value_sampler.py`:** To generate series of payments, gradient parameters, or multiple related loan/payment amounts.
|
||||
|
||||
These represent a next level of complexity beyond the current system's direct formula evaluation approach. A `docs/` folder could be created in the future to detail potential architectures for these advanced features.
|
||||
|
||||
## Known Issues / Potential Refinements
|
||||
|
||||
* **Banker's Discount - Negative Proceeds:** For the `BANKERS_DISCOUNT_SOLVE_FOR_PROCEEDS` concept, it's possible to generate scenarios where the calculated discount amount is larger than the maturity value, resulting in negative proceeds. While mathematically possible, this is often an undesirable outcome for typical practice problems. A future refinement could involve adding more specific plausibility checks in `problem_engine.py` to resample input values (like discount rate or time) to ensure positive proceeds.
|
||||
* **Plausibility of Complex Scenarios:** As more complex multi-step problems are considered (like Equation of Value), ensuring the generated numbers lead to "sensible" real-world scenarios will require more sophisticated plausibility checks during value sampling.
|
||||
|
||||
489
building_blocks/financial_concepts.json
Normal file
489
building_blocks/financial_concepts.json
Normal file
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
69
data/names.json
Normal file
69
data/names.json
Normal file
@ -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"
|
||||
]
|
||||
}
|
||||
268
data/text_snippets.json
Normal file
268
data/text_snippets.json
Normal file
@ -0,0 +1,268 @@
|
||||
{
|
||||
"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: {target_variable_lhs} = {formula_symbolic_rhs}",
|
||||
"substitute_values": "Now, we substitute the known values: {target_variable_lhs} = {formula_with_values_rhs}",
|
||||
"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}.",
|
||||
"identify_gradient_parameters": "Identify the parameters of the arithmetic gradient series:",
|
||||
"state_formula_annuity_component": "The formula for the present worth of the base annuity component (P_A) is: P_A = A1 * (P/A, i, N) which is A1 * [((1 + i)^N - 1) / (i * (1 + i)^N)]",
|
||||
"calculate_pv_annuity_component": "Calculating the present worth of the base annuity component (P_A):",
|
||||
"state_formula_gradient_component": "The formula for the present worth of the arithmetic gradient component (P_G) is: P_G = G * (P/G, i, N) which is G * [(((1 + i)^N - (i * N) - 1) / (i^2 * (1 + i)^N))]",
|
||||
"calculate_pv_gradient_component": "Calculating the present worth of the arithmetic gradient component (P_G):",
|
||||
"state_formula_total_pv": "The total present worth (P_total) is the sum of the present worth of the annuity and gradient components: P_total = P_A + P_G",
|
||||
"sum_pv_components": "Summing the present worth components:"
|
||||
},
|
||||
"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",
|
||||
"A1_base_annuity": "base annuity amount",
|
||||
"G_gradient_amount": "arithmetic gradient amount",
|
||||
"N_periods": "number of periods",
|
||||
"P_A_component": "present worth of the annuity component",
|
||||
"P_G_component": "present worth of the gradient component",
|
||||
"P_gradient_series": "total present worth of the arithmetic gradient series",
|
||||
"I_simple": "simple interest amount",
|
||||
"F_simple": "future value with simple interest",
|
||||
"F_compound": "future value with compound interest",
|
||||
"t_years": "time period in years (for compounding)",
|
||||
"i_simple_equivalent": "equivalent simple interest rate",
|
||||
"F_maturity": "maturity value (for Banker's Discount)",
|
||||
"P_proceeds": "proceeds from discounted loan",
|
||||
"Db_discount_amount": "banker's discount amount",
|
||||
"F_continuous": "future value with continuous compounding"
|
||||
},
|
||||
"compounding_frequency_adverbs": {
|
||||
"annually": "annually",
|
||||
"semi-annually": "semi-annually",
|
||||
"quarterly": "quarterly",
|
||||
"monthly": "monthly",
|
||||
"bi-monthly": "bi-monthly",
|
||||
"semi-monthly": "semi-monthly",
|
||||
"continuously": "continuously"
|
||||
}
|
||||
}
|
||||
155
data/value_ranges.json
Normal file
155
data/value_ranges.json
Normal file
@ -0,0 +1,155 @@
|
||||
{
|
||||
"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
|
||||
},
|
||||
"interest_rate_per_period_effective": {
|
||||
"min": 0.005,
|
||||
"max": 0.05,
|
||||
"unit_display": "% per period",
|
||||
"internal_precision": 8,
|
||||
"display_precision": 3
|
||||
},
|
||||
"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": 60,
|
||||
"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
|
||||
},
|
||||
"date_period_generation": {
|
||||
"description": "Parameters for generating random date periods.",
|
||||
"min_days": 30,
|
||||
"max_days": 730,
|
||||
"base_year_start": 1990,
|
||||
"base_year_end": 2030
|
||||
}
|
||||
}
|
||||
57
main.py
Normal file
57
main.py
Normal file
@ -0,0 +1,57 @@
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Ensure the src directory is in the Python path
|
||||
# This allows running main.py from the project root (e.g., python main.py)
|
||||
# and having imports like 'from src import ...' work correctly.
|
||||
project_root = os.path.abspath(os.path.dirname(__file__))
|
||||
src_path = os.path.join(project_root, 'src')
|
||||
if src_path not in sys.path:
|
||||
sys.path.insert(0, src_path)
|
||||
if project_root not in sys.path: # Also add project root for 'from src import ...' if main is outside src
|
||||
sys.path.insert(0, project_root)
|
||||
|
||||
|
||||
from src.problem_engine import generate_problem
|
||||
from src.data_loader import get_text_snippets # For variable descriptions
|
||||
# Note: problem_engine.generate_problem() handles caching of all necessary data internally.
|
||||
|
||||
def main():
|
||||
print("Generating a financial problem via main.py...\n")
|
||||
|
||||
# Load text snippets for variable descriptions
|
||||
# generate_problem() will load its own cache, but for descriptions here, we need it too.
|
||||
# A more advanced setup might pass TEXT_SNIPPETS_DATA around or make it a singleton.
|
||||
text_snippets_data = get_text_snippets()
|
||||
if not text_snippets_data:
|
||||
print("Error: Failed to load text snippets for main.py. Exiting.")
|
||||
return
|
||||
|
||||
problem = generate_problem()
|
||||
|
||||
if "error" in problem:
|
||||
print(f"Error generating problem: {problem['error']}")
|
||||
else:
|
||||
print(f"--- Problem ---")
|
||||
print(f"Concept ID: {problem['concept_id']}")
|
||||
print(f"Topic: {problem['topic']}")
|
||||
|
||||
print("\nProblem Statement:")
|
||||
print(problem['problem_statement'])
|
||||
|
||||
input("\nPress Enter to see the question and solution...") # Wait for user interaction
|
||||
|
||||
unknown_key_desc = text_snippets_data.get("variable_descriptions", {}).get(problem['target_unknown_key'], problem['target_unknown_key'])
|
||||
print(f"\nQuestion: What is the {unknown_key_desc}?")
|
||||
|
||||
print("\nGuided Solution:")
|
||||
for i, step in enumerate(problem['solution_steps']):
|
||||
# The steps from solution_presenter already have numbering.
|
||||
# If not, add: print(f" {i+1}. {step}")
|
||||
print(step)
|
||||
|
||||
print(f"\nFinal Answer ({problem['target_unknown_key']}): {problem['calculated_answer_formatted']}")
|
||||
print("---------------------------------------\n")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
7
pyproject.toml
Normal file
7
pyproject.toml
Normal file
@ -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 = []
|
||||
63
src/data_loader.py
Normal file
63
src/data_loader.py
Normal file
@ -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))
|
||||
133
src/date_utils.py
Normal file
133
src/date_utils.py
Normal file
@ -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.")
|
||||
123
src/formula_evaluator.py
Normal file
123
src/formula_evaluator.py
Normal file
@ -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.")
|
||||
209
src/narrative_builder.py
Normal file
209
src/narrative_builder.py
Normal file
@ -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)
|
||||
345
src/problem_engine.py
Normal file
345
src/problem_engine.py
Normal file
@ -0,0 +1,345 @@
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||
|
||||
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
|
||||
"F_exact_simple": "future_value",
|
||||
"F_ordinary_simple": "future_value",
|
||||
"I_simple": "interest_amount",
|
||||
"I_exact_simple": "interest_amount",
|
||||
"I_ordinary_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",
|
||||
"i_simple_equivalent": "simple_interest_rate_annual", # Added for CONTINUOUS_COMPOUNDING_EQUIVALENT_SIMPLE_RATE
|
||||
"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
|
||||
# Convert required_knowns to a set for efficient checking and modification
|
||||
required_knowns = set(selected_concept.get("required_knowns_for_target", []))
|
||||
handled_vars = set()
|
||||
|
||||
# --- BEGIN DATE SPECIFIC LOGIC ---
|
||||
# Check if the concept requires start_date and end_date
|
||||
if "start_date" in required_knowns and "end_date" in required_knowns:
|
||||
# Fetch date generation parameters from value_ranges.json, with defaults
|
||||
date_period_config = VALUE_RANGES_CACHE.get("date_period_generation", {})
|
||||
min_days_val = date_period_config.get("min_days", 30)
|
||||
max_days_val = date_period_config.get("max_days", 730)
|
||||
base_year_start_val = date_period_config.get("base_year_start", 1990)
|
||||
base_year_end_val = date_period_config.get("base_year_end", 2030)
|
||||
|
||||
start_date_obj, end_date_obj, num_days_val = date_utils.get_random_date_period(
|
||||
min_days=min_days_val,
|
||||
max_days=max_days_val,
|
||||
base_year_range=(base_year_start_val, base_year_end_val)
|
||||
)
|
||||
|
||||
# Populate all_variables_data_formatted and formula_context_vars
|
||||
all_variables_data_formatted["start_date"] = {'key': 'start_date', 'value': start_date_obj, 'unit': 'date', 'display_precision': None}
|
||||
all_variables_data_formatted["end_date"] = {'key': 'end_date', 'value': end_date_obj, 'unit': 'date', 'display_precision': None}
|
||||
all_variables_data_formatted["n_time_days"] = {'key': 'n_time_days', 'value': num_days_val, 'unit': 'days', 'display_precision': 0}
|
||||
|
||||
formula_context_vars["start_date"] = start_date_obj
|
||||
formula_context_vars["end_date"] = end_date_obj
|
||||
formula_context_vars["n_time_days"] = num_days_val
|
||||
handled_vars.update(["start_date", "end_date", "n_time_days"]) # Mark these as handled
|
||||
|
||||
# Determine time_base_days and potentially time_base_year_for_exact based on financial_topic
|
||||
time_base_days_val = 0
|
||||
time_base_year_for_exact_val = None
|
||||
|
||||
if selected_concept["financial_topic"] == "Exact Simple Interest":
|
||||
time_base_year_for_exact_val = start_date_obj.year
|
||||
time_base_days_val = date_utils.days_in_year(time_base_year_for_exact_val)
|
||||
|
||||
all_variables_data_formatted["time_base_year_for_exact"] = {'key': 'time_base_year_for_exact', 'value': time_base_year_for_exact_val, 'unit': 'year', 'display_precision': 0}
|
||||
formula_context_vars["time_base_year_for_exact"] = time_base_year_for_exact_val
|
||||
# This variable is derived for solution steps, not typically in 'required_knowns_for_target'
|
||||
|
||||
elif selected_concept["financial_topic"] == "Ordinary Simple Interest":
|
||||
time_base_days_val = 360
|
||||
|
||||
# Add time_base_days if it was determined (for Exact or Ordinary)
|
||||
if time_base_days_val > 0:
|
||||
all_variables_data_formatted["time_base_days"] = {'key': 'time_base_days', 'value': time_base_days_val, 'unit': 'days', 'display_precision': 0}
|
||||
formula_context_vars["time_base_days"] = time_base_days_val
|
||||
# This variable is derived for formulas/solution steps, not typically in 'required_knowns_for_target'
|
||||
# --- END DATE SPECIFIC LOGIC ---
|
||||
|
||||
# Loop through all required knowns for the concept
|
||||
for var_key in required_knowns: # Iterate over the original set of required knowns
|
||||
if var_key in handled_vars:
|
||||
continue # Skip if already handled by the date-specific logic
|
||||
|
||||
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
|
||||
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
|
||||
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']}'"}
|
||||
|
||||
# --- BEGIN PLAUSIBILITY CHECKS (e.g., for rate solving) ---
|
||||
if selected_concept["concept_id"] in ["COMPOUND_INTEREST_SOLVE_FOR_RATE", "CONTINUOUS_COMPOUNDING_SOLVE_FOR_RATE", "SIMPLE_INTEREST_SOLVE_FOR_RATE_FROM_I"]:
|
||||
# Ensure F > P to avoid negative or extremely low rates, unless P and F are very close.
|
||||
# This logic assumes 'P' and 'F_compound'/'F_continuous'/'F_simple' are the relevant keys.
|
||||
# For SIMPLE_INTEREST_SOLVE_FOR_RATE_FROM_I, it uses P and I_simple. We'd need a different check if I_simple could be negative.
|
||||
|
||||
p_key, f_key = None, None
|
||||
if "P" in formula_context_vars:
|
||||
p_key = "P"
|
||||
if "F_compound" in formula_context_vars: f_key = "F_compound"
|
||||
elif "F_continuous" in formula_context_vars: f_key = "F_continuous"
|
||||
elif "F_simple" in formula_context_vars: f_key = "F_simple"
|
||||
# For SIMPLE_INTEREST_SOLVE_FOR_RATE_FROM_I, F is not directly used, I_simple is.
|
||||
# We assume I_simple will be positive. If I_simple is negative, rate will be negative.
|
||||
|
||||
if p_key and f_key:
|
||||
max_resample_attempts = 5
|
||||
attempt = 0
|
||||
# We want F > P for a positive growth rate.
|
||||
# If F is very close to P, rate will be very small, which is fine.
|
||||
# If F < P, rate will be negative. Let's try to avoid large negative rates by ensuring F is not drastically smaller than P.
|
||||
# A simple approach: if F < P, resample F until F > P * 0.8 (allowing for some negative rates but not extreme ones).
|
||||
# Or, more simply for now, ensure F > P for typical positive rate problems.
|
||||
while formula_context_vars[f_key] <= formula_context_vars[p_key] and attempt < max_resample_attempts:
|
||||
print(f"Resampling {f_key} because {f_key} ({formula_context_vars[f_key]}) <= {p_key} ({formula_context_vars[p_key]}) for rate calculation.")
|
||||
f_value_range_key = CONCEPT_VAR_TO_VALUERANGE_KEY.get(f_key)
|
||||
if f_value_range_key:
|
||||
var_data_f = value_sampler.get_value_for_variable(f_value_range_key)
|
||||
if var_data_f:
|
||||
all_variables_data_formatted[f_key] = var_data_f
|
||||
formula_context_vars[f_key] = var_data_f["value"]
|
||||
else: # Failed to resample F, break to avoid infinite loop
|
||||
break
|
||||
else: # No range key for F, break
|
||||
break
|
||||
attempt += 1
|
||||
if formula_context_vars[f_key] <= formula_context_vars[p_key]:
|
||||
print(f"Warning: Could not ensure {f_key} > {p_key} after {max_resample_attempts} attempts for concept {selected_concept['concept_id']}. Proceeding with current values.")
|
||||
|
||||
# --- END PLAUSIBILITY CHECKS ---
|
||||
|
||||
# 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()
|
||||
|
||||
# Load all concepts
|
||||
all_concepts = data_loader.get_financial_concepts()
|
||||
if not all_concepts:
|
||||
print("Failed to load financial concepts for testing. Exiting.")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Found {len(all_concepts)} concepts to test.\n")
|
||||
|
||||
original_financial_concepts_global = FINANCIAL_CONCEPTS # Save the global state if it was set
|
||||
|
||||
for i, concept_to_test in enumerate(all_concepts):
|
||||
# Temporarily set the global FINANCIAL_CONCEPTS to only the current concept for generate_problem()
|
||||
# This ensures generate_problem picks this specific concept.
|
||||
FINANCIAL_CONCEPTS = [concept_to_test]
|
||||
|
||||
print(f"\n--- Testing Concept {i+1}/{len(all_concepts)}: {concept_to_test['concept_id']} ---")
|
||||
problem = generate_problem() # generate_problem will use the modified global FINANCIAL_CONCEPTS
|
||||
|
||||
if "error" in problem:
|
||||
print(f"Error generating problem: {problem['error']}")
|
||||
else:
|
||||
print(f"Topic: {problem['topic']}")
|
||||
print("\nProblem Statement:")
|
||||
print(problem['problem_statement'])
|
||||
|
||||
# Ensure TEXT_SNIPPETS_DATA is loaded for the question part
|
||||
if TEXT_SNIPPETS_DATA is None: # Should have been loaded by _load_all_data_cached
|
||||
_load_all_data_cached()
|
||||
|
||||
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")
|
||||
|
||||
FINANCIAL_CONCEPTS = original_financial_concepts_global # Restore original global state
|
||||
print("Completed testing all concepts.")
|
||||
345
src/solution_presenter.py
Normal file
345
src/solution_presenter.py
Normal file
@ -0,0 +1,345 @@
|
||||
import random
|
||||
import re # Added for regex substitution
|
||||
import decimal # Added for precise formatting of numbers in formulas
|
||||
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_value_for_formula(raw_value, var_data):
|
||||
"""
|
||||
Formats a raw numerical value for substitution into a formula string.
|
||||
Rates are kept as decimals. Uses a higher precision for rates,
|
||||
otherwise display_precision from var_data.
|
||||
"""
|
||||
unit_display_str = var_data.get("unit_display")
|
||||
unit_str = var_data.get("unit")
|
||||
|
||||
is_rate = (isinstance(unit_display_str, str) and "%" in unit_display_str) or \
|
||||
(isinstance(unit_str, str) and "%" in unit_str)
|
||||
|
||||
if isinstance(raw_value, (float, decimal.Decimal)):
|
||||
if is_rate:
|
||||
# Use a higher, fixed precision for rates in formulas
|
||||
# to maintain accuracy for manual calculation from the substituted formula.
|
||||
# The internal_precision from value_ranges.json (e.g., 8) is for generation.
|
||||
# For substitution display, 4 to 6 decimal places for the rate decimal is usually sufficient.
|
||||
# Let's use 4 as a balance, as rates are often quoted to 2 decimal places as percentages.
|
||||
# So, 0.123456 (12.3456%) would be 0.1235 if display_precision for % is 2,
|
||||
# but for formula, we want 0.123456 or similar.
|
||||
# Using 4 for the decimal: 0.0737 -> 0.0737, 0.1673 -> 0.1673
|
||||
# Let's use 6 to be safer and show more of the internal value.
|
||||
return f"{raw_value:.6f}"
|
||||
else:
|
||||
precision = var_data.get('display_precision')
|
||||
if precision is not None:
|
||||
return f"{raw_value:.{precision}f}"
|
||||
else: # Default precision for non-rate floats if not specified
|
||||
return f"{raw_value:.6f}"
|
||||
elif isinstance(raw_value, int):
|
||||
return str(raw_value)
|
||||
else: # Fallback for other types (e.g. already a string, though less likely here)
|
||||
return str(raw_value)
|
||||
|
||||
def generate_guided_solution(financial_concept, all_variables_data, calculated_solution_data):
|
||||
"""
|
||||
Generates a step-by-step guided solution.
|
||||
"""
|
||||
solution_steps_text = []
|
||||
step_counter = 1
|
||||
|
||||
target_unknown_key = financial_concept["target_unknown"]
|
||||
symbolic_formula_rhs = financial_concept["formulas"].get(target_unknown_key, "Formula not defined")
|
||||
|
||||
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}"
|
||||
|
||||
if step_key_path == "solution_guidance.identify_knowns":
|
||||
knowns_list_text = [f"{step_counter}. {step_text_template}"]
|
||||
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)}")
|
||||
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
|
||||
|
||||
elif step_key_path == "solution_guidance.state_formula":
|
||||
formula_rhs_to_display = symbolic_formula_rhs
|
||||
# Special handling for complex formulas to show a more user-friendly version
|
||||
if target_unknown_key == "i_rate_per_period" and "COMPOUND_INTEREST_SOLVE_FOR_RATE" in financial_concept["concept_id"]:
|
||||
formula_rhs_to_display = "(F/P)^(1/n) - 1" # r = i * m is handled as a subsequent step if needed
|
||||
elif target_unknown_key == "n_total_compounding_periods" and "COMPOUND_INTEREST_SOLVE_FOR_TIME" in financial_concept["concept_id"]:
|
||||
formula_rhs_to_display = "log(F/P) / log(1+i)" # t = n / m is handled as a subsequent step
|
||||
|
||||
formatted_step_text = formatted_step_text.format(
|
||||
target_variable_lhs=target_unknown_key,
|
||||
formula_symbolic_rhs=formula_rhs_to_display
|
||||
)
|
||||
|
||||
elif step_key_path == "solution_guidance.substitute_values":
|
||||
formula_with_values_rhs = symbolic_formula_rhs
|
||||
# Iterate through all variables that might be in the formula
|
||||
# Sort by length of key, descending, to replace longer keys first (e.g. "n_time_days" before "n")
|
||||
sorted_var_keys = sorted(all_variables_data.keys(), key=len, reverse=True)
|
||||
|
||||
for var_key in sorted_var_keys:
|
||||
if var_key in formula_with_values_rhs: # Check if key is part of formula string
|
||||
var_data = all_variables_data[var_key]
|
||||
# Format the raw value for formula substitution (decimal for rates, specific precision)
|
||||
value_for_formula = _format_value_for_formula(var_data['value'], var_data)
|
||||
# Use regex to replace whole words only
|
||||
formula_with_values_rhs = re.sub(r'\b' + re.escape(var_key) + r'\b', value_for_formula, formula_with_values_rhs)
|
||||
|
||||
formatted_step_text = formatted_step_text.format(
|
||||
target_variable_lhs=target_unknown_key,
|
||||
formula_with_values_rhs=formula_with_values_rhs
|
||||
)
|
||||
|
||||
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:
|
||||
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=_format_value_for_formula(original_time_var['value'], original_time_var),
|
||||
original_time_unit=original_time_unit,
|
||||
converted_time_value_years=_format_value_for_formula(all_variables_data['n_time_years']['value'], all_variables_data['n_time_years'])
|
||||
)
|
||||
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=_format_value_for_formula(all_variables_data['r_nominal_annual']['value'], all_variables_data['r_nominal_annual']),
|
||||
compounding_periods_per_year=all_variables_data['m_compounding_periods_per_year']['value'],
|
||||
interest_rate_per_period_decimal=_format_value_for_formula(all_variables_data['i_rate_per_period']['value'], all_variables_data['i_rate_per_period'])
|
||||
)
|
||||
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=_format_value_for_formula(all_variables_data['t_years']['value'], all_variables_data['t_years']),
|
||||
compounding_periods_per_year=all_variables_data['m_compounding_periods_per_year']['value'],
|
||||
total_periods=_format_value_for_formula(all_variables_data['n_total_compounding_periods']['value'], all_variables_data['n_total_compounding_periods'])
|
||||
)
|
||||
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=_format_value_for_formula(all_variables_data['n_time_years_fractional']['value'], all_variables_data['n_time_years_fractional'])
|
||||
)
|
||||
else: continue
|
||||
|
||||
elif step_key_path == "solution_guidance.intermediate_step":
|
||||
# This needs to be more robust or specific step keys should be used.
|
||||
intermediate_var_name = None
|
||||
intermediate_var_data = None
|
||||
|
||||
if "Db_discount_amount" in all_variables_data and "BANKERS_DISCOUNT" in financial_concept["concept_id"]:
|
||||
intermediate_var_name = "Db_discount_amount"
|
||||
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"
|
||||
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"
|
||||
|
||||
if intermediate_var_name and intermediate_var_name in all_variables_data:
|
||||
intermediate_var_data = all_variables_data[intermediate_var_name]
|
||||
intermediate_var_desc = get_snippet(f"variable_descriptions.{intermediate_var_name}")
|
||||
# For i_rate_per_period, we want to show more precision and not as percent here
|
||||
if intermediate_var_name == "i_rate_per_period":
|
||||
display_val = _format_value_for_formula(intermediate_var_data['value'], intermediate_var_data)
|
||||
elif intermediate_var_name == "n_total_compounding_periods":
|
||||
display_val = f"{_format_value_for_formula(intermediate_var_data['value'], intermediate_var_data)} periods"
|
||||
else: # General case, use standard display formatting
|
||||
display_val = format_value_for_display(intermediate_var_data)
|
||||
|
||||
formatted_step_text = formatted_step_text.format(step_name=intermediate_var_desc) + f" {display_val}"
|
||||
else:
|
||||
# Fallback or skip if no clear intermediate variable is identified for this generic step
|
||||
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
|
||||
# from src.value_sampler import format_value_for_display # Already imported at top level
|
||||
|
||||
# Mock data for testing
|
||||
mock_snippets_data_for_test = { # Renamed to avoid conflict if this file is imported elsewhere
|
||||
"solution_guidance": {
|
||||
"identify_knowns": "First, identify knowns:",
|
||||
"state_formula": "The relevant formula for this problem is: {target_variable_lhs} = {formula_symbolic_rhs}",
|
||||
"substitute_values": "Now, we substitute the known values: {target_variable_lhs} = {formula_with_values_rhs}",
|
||||
"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": "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.",
|
||||
"intermediate_step": "The intermediate result for {step_name} is:",
|
||||
"days_in_period": "The number of days from {start_date} to {end_date} is {number_of_days} days.",
|
||||
"check_leap_year": "{year} is {is_or_is_not} a leap year.",
|
||||
"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}."
|
||||
},
|
||||
"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 compounding period",
|
||||
"n_total_compounding_periods": "total number of 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)"
|
||||
}
|
||||
}
|
||||
|
||||
original_get_snippets = _get_text_snippets_cached # Save original
|
||||
_get_text_snippets_cached = lambda: mock_snippets_data_for_test # Override for this test block
|
||||
|
||||
# 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", # Intermediate
|
||||
"I_exact_simple": "P * i_simple_annual * n_time_years_fractional" # Target
|
||||
},
|
||||
"required_knowns_for_target": ["P", "i_simple_annual", "start_date", "end_date"],
|
||||
# Note: n_time_days, time_base_days, time_base_year_for_exact are derived by problem_engine
|
||||
# n_time_years_fractional is an intermediate calculation based on the above formula
|
||||
"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", # This step calculates n_time_years_fractional
|
||||
"solution_guidance.state_formula", # Shows I_exact_simple = P * i_simple_annual * n_time_years_fractional
|
||||
"solution_guidance.substitute_values", # Shows I_exact_simple = 5000.00 * 0.05 * 0.495890
|
||||
"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': 4}, # Rate, use 4 for decimal 0.0500
|
||||
"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},
|
||||
"time_base_year_for_exact": {'key': 'time_base_year_for_exact', 'value': 2023, 'display_precision': 0},
|
||||
"time_base_days": {'key': 'time_base_days', 'value': 365, 'unit': 'days', 'display_precision': 0},
|
||||
"n_time_years_fractional": {'key': 'n_time_years_fractional', 'value': 181/365, 'display_precision': 6}, # This is calculated
|
||||
"I_exact_simple": {'key': 'I_exact_simple', 'value': 5000 * 0.05 * (181/365), 'currency': 'Php', 'display_precision': 2} # Final answer
|
||||
}
|
||||
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)
|
||||
|
||||
_get_text_snippets_cached = original_get_snippets # Restore original
|
||||
179
src/value_sampler.py
Normal file
179
src/value_sampler.py
Normal file
@ -0,0 +1,179 @@
|
||||
import random
|
||||
import decimal
|
||||
import datetime # Added for type checking
|
||||
from src.data_loader import get_value_ranges
|
||||
from src import date_utils # Added for date formatting
|
||||
|
||||
# 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"
|
||||
"""
|
||||
# --- BEGIN DEBUG PRINT ---
|
||||
# print(f"DEBUG: format_value_for_display received value_data: {value_data}")
|
||||
# --- END DEBUG PRINT ---
|
||||
|
||||
if not isinstance(value_data, dict) or 'value' not in value_data:
|
||||
# print(f"DEBUG: Fallback str(value_data): {str(value_data)}")
|
||||
return str(value_data) # Fallback
|
||||
|
||||
val = value_data['value']
|
||||
display_precision = value_data.get('display_precision')
|
||||
|
||||
# --- BEGIN DEBUG PRINT ---
|
||||
# print(f"DEBUG: val: {val}, display_precision: {display_precision}, type(val): {type(val)}")
|
||||
# --- END DEBUG PRINT ---
|
||||
|
||||
# Handle date objects first
|
||||
if isinstance(val, datetime.date):
|
||||
# print(f"DEBUG: Path: Date. Result: {date_utils.format_date_for_display(val)}")
|
||||
return date_utils.format_date_for_display(val)
|
||||
|
||||
# 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 value_data.get("unit_display") and "%" in value_data["unit_display"]: # Check if unit_display exists and is not None
|
||||
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 and value_data["currency"] is not None: # Ensure currency is not None
|
||||
# print(f"DEBUG: Path: Currency. Result: {value_data['currency']} {formatted_num}")
|
||||
return f"{value_data['currency']} {formatted_num}"
|
||||
elif "unit_display" in value_data and value_data["unit_display"] is not None: # Ensure unit_display is not None
|
||||
# print(f"DEBUG: Path: Unit Display. Result: {formatted_num}{value_data['unit_display']}")
|
||||
return f"{formatted_num}{value_data['unit_display']}"
|
||||
elif "unit" in value_data and value_data["unit"] is not None: # Ensure unit is not None
|
||||
# print(f"DEBUG: Path: Unit. Result: {formatted_num} {value_data['unit']}")
|
||||
return f"{formatted_num} {value_data['unit']}"
|
||||
else:
|
||||
# print(f"DEBUG: Path: Formatted Num Only. Result: {formatted_num}")
|
||||
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)}")
|
||||
Loading…
x
Reference in New Issue
Block a user