This commit marks a major milestone in the problem generator project.
Key systems include:
- Fully functional problem generation for all 23 defined concepts (Simple Interest, Compound Interest, Banker's Discount, Effective Rate, Continuous Compounding, Exact/Ordinary Simple Interest).
- Robust date handling for date-specific interest calculations.
- Improved solution presentation with accurate "Substitute Values" step and complete variable descriptions.
- Interactive problem display in `main.py`: problem statement shown first, question and solution revealed on user input.
- Added plausibility checks for rate calculations to avoid unrealistic negative rates.
- Comprehensive `README.md` update:
- Detailed system architecture (modules, data files, Mermaid diagram).
- List of all currently covered financial concepts.
- Instructions for running and extending the generator.
- Discussion of scope for future enhancements (Equation of Value, Gradients, etc.).
- Refinements to `value_sampler.py` for better formatting and handling of None values.
- Updates to `text_snippets.json` for complete variable descriptions and improved solution step phrasing.
- Updates to `value_ranges.json` for date generation parameters.
- `problem_engine.py` now systematically tests all concepts when run directly.
- Added `uv.lock` to track resolved dependencies.
The system is capable of generating a wide variety of engineering economy problems with detailed, step-by-step solutions and an interactive user experience.
180 lines
8.4 KiB
Python
180 lines
8.4 KiB
Python
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)}")
|