|
4 | 4 | import logging |
5 | 5 | import os |
6 | 6 | from datetime import date, datetime |
7 | | -from decimal import Decimal |
| 7 | +from decimal import Decimal, InvalidOperation |
8 | 8 | from typing import Any, Optional, Union |
9 | 9 |
|
10 | 10 | from django import template |
@@ -413,54 +413,161 @@ def internal_link(link, text) -> str: |
413 | 413 | return mark_safe(f'<a href="{url}">{text}</a>') |
414 | 414 |
|
415 | 415 |
|
416 | | -def destringify(value: Any) -> Any: |
417 | | - """Convert a string value into a float. |
| 416 | +def make_decimal(value: Any) -> Any: |
| 417 | + """Convert an input value into a Decimal. |
418 | 418 |
|
419 | | - - If the value is a string, attempt to convert it to a float. |
420 | | - - If conversion fails, return the original string. |
421 | | - - If the value is not a string, return it unchanged. |
| 419 | + - Converts [string, int, float] types into Decimal |
| 420 | + - If conversion fails, returns the original value |
422 | 421 |
|
423 | 422 | The purpose of this function is to provide "seamless" math operations in templates, |
424 | 423 | where numeric values may be provided as strings, or converted to strings during template rendering. |
425 | 424 | """ |
426 | | - if isinstance(value, str): |
427 | | - value = value.strip() |
| 425 | + if any(isinstance(value, t) for t in [int, float, str]): |
428 | 426 | try: |
429 | | - return float(value) |
430 | | - except ValueError: |
431 | | - return value |
| 427 | + value = Decimal(str(value).strip()) |
| 428 | + except (InvalidOperation, TypeError, ValueError): |
| 429 | + logger.warning( |
| 430 | + 'make_decimal: Failed to convert value to Decimal: %s (%s)', |
| 431 | + value, |
| 432 | + type(value), |
| 433 | + ) |
432 | 434 |
|
433 | 435 | return value |
434 | 436 |
|
435 | 437 |
|
| 438 | +def cast_to_type(value: Any, cast: type) -> Any: |
| 439 | + """Attempt to cast a value to the provided type. |
| 440 | +
|
| 441 | + If casting fails, the original value is returned. |
| 442 | + """ |
| 443 | + if cast is not None: |
| 444 | + try: |
| 445 | + value = cast(value) |
| 446 | + except (ValueError, TypeError): |
| 447 | + pass |
| 448 | + |
| 449 | + return value |
| 450 | + |
| 451 | + |
| 452 | +def debug_vars(x: Any, y: Any) -> str: |
| 453 | + """Return a debug string showing the types and values of two variables.""" |
| 454 | + return f": x='{x}' ({type(x).__name__}), y='{y}' ({type(y).__name__})" |
| 455 | + |
| 456 | + |
436 | 457 | @register.simple_tag() |
437 | | -def add(x: Any, y: Any) -> Any: |
438 | | - """Add two numbers (or number like values) together.""" |
439 | | - return destringify(x) + destringify(y) |
| 458 | +def add(x: Any, y: Any, cast: Optional[type] = None) -> Any: |
| 459 | + """Add two numbers (or number like values) together. |
| 460 | +
|
| 461 | + Arguments: |
| 462 | + x: The first value to add |
| 463 | + y: The second value to add |
| 464 | + cast: Optional type to cast the result to (e.g. int, float, str) |
| 465 | +
|
| 466 | + Raises: |
| 467 | + ValidationError: If the values cannot be added together |
| 468 | + """ |
| 469 | + try: |
| 470 | + result = make_decimal(x) + make_decimal(y) |
| 471 | + except (InvalidOperation, TypeError, ValueError): |
| 472 | + raise ValidationError( |
| 473 | + _('Cannot add values of incompatible types') + debug_vars(x, y) |
| 474 | + ) |
| 475 | + return cast_to_type(result, cast) |
440 | 476 |
|
441 | 477 |
|
442 | 478 | @register.simple_tag() |
443 | | -def subtract(x: Any, y: Any) -> Any: |
444 | | - """Subtract one number (or number-like value) from another.""" |
445 | | - return destringify(x) - destringify(y) |
| 479 | +def subtract(x: Any, y: Any, cast: Optional[type] = None) -> Any: |
| 480 | + """Subtract one number (or number-like value) from another. |
| 481 | +
|
| 482 | + Arguments: |
| 483 | + x: The value to be subtracted from |
| 484 | + y: The value to be subtracted |
| 485 | + cast: Optional type to cast the result to (e.g. int, float, str) |
| 486 | +
|
| 487 | + Raises: |
| 488 | + ValidationError: If the values cannot be subtracted |
| 489 | + """ |
| 490 | + try: |
| 491 | + result = make_decimal(x) - make_decimal(y) |
| 492 | + except (InvalidOperation, TypeError, ValueError): |
| 493 | + raise ValidationError( |
| 494 | + _('Cannot subtract values of incompatible types') + debug_vars(x, y) |
| 495 | + ) |
| 496 | + |
| 497 | + return cast_to_type(result, cast) |
446 | 498 |
|
447 | 499 |
|
448 | 500 | @register.simple_tag() |
449 | | -def multiply(x: Any, y: Any) -> Any: |
450 | | - """Multiply two numbers (or number-like values) together.""" |
451 | | - return destringify(x) * destringify(y) |
| 501 | +def multiply(x: Any, y: Any, cast: Optional[type] = None) -> Any: |
| 502 | + """Multiply two numbers (or number-like values) together. |
| 503 | +
|
| 504 | + Arguments: |
| 505 | + x: The first value to multiply |
| 506 | + y: The second value to multiply |
| 507 | + cast: Optional type to cast the result to (e.g. int, float, str) |
| 508 | +
|
| 509 | + Raises: |
| 510 | + ValidationError: If the values cannot be multiplied together |
| 511 | + """ |
| 512 | + try: |
| 513 | + result = make_decimal(x) * make_decimal(y) |
| 514 | + except (InvalidOperation, TypeError, ValueError): |
| 515 | + raise ValidationError( |
| 516 | + _('Cannot multiply values of incompatible types') + debug_vars(x, y) |
| 517 | + ) |
| 518 | + |
| 519 | + return cast_to_type(result, cast) |
452 | 520 |
|
453 | 521 |
|
454 | 522 | @register.simple_tag() |
455 | | -def divide(x: Any, y: Any) -> Any: |
456 | | - """Divide one number (or number-like value) by another.""" |
457 | | - return destringify(x) / destringify(y) |
| 523 | +def divide(x: Any, y: Any, cast: Optional[type] = None) -> Any: |
| 524 | + """Divide one number (or number-like value) by another. |
| 525 | +
|
| 526 | + Arguments: |
| 527 | + x: The value to be divided |
| 528 | + y: The value to divide by |
| 529 | + cast: Optional type to cast the result to (e.g. int, float, str) |
| 530 | +
|
| 531 | + Raises: |
| 532 | + ValidationError: If the values cannot be divided |
| 533 | + """ |
| 534 | + try: |
| 535 | + result = make_decimal(x) / make_decimal(y) |
| 536 | + except (InvalidOperation, TypeError, ValueError): |
| 537 | + raise ValidationError( |
| 538 | + _('Cannot divide values of incompatible types') + debug_vars(x, y) |
| 539 | + ) |
| 540 | + except ZeroDivisionError: |
| 541 | + raise ValidationError(_('Cannot divide by zero') + debug_vars(x, y)) |
| 542 | + |
| 543 | + return cast_to_type(result, cast) |
458 | 544 |
|
459 | 545 |
|
460 | 546 | @register.simple_tag() |
461 | | -def modulo(x: Any, y: Any) -> Any: |
462 | | - """Calculate the modulo of one number (or number-like value) by another.""" |
463 | | - return destringify(x) % destringify(y) |
| 547 | +def modulo(x: Any, y: Any, cast: Optional[type] = None) -> Any: |
| 548 | + """Calculate the modulo of one number (or number-like value) by another. |
| 549 | +
|
| 550 | + Arguments: |
| 551 | + x: The first value to be used in the modulo operation |
| 552 | + y: The second value to be used in the modulo operation |
| 553 | + cast: Optional type to cast the result to (e.g. int, float, str) |
| 554 | +
|
| 555 | + Raises: |
| 556 | + ValidationError: If the values cannot be used in a modulo operation |
| 557 | + """ |
| 558 | + try: |
| 559 | + result = make_decimal(x) % make_decimal(y) |
| 560 | + except (InvalidOperation, TypeError, ValueError): |
| 561 | + raise ValidationError( |
| 562 | + _('Cannot perform modulo operation with values of incompatible types') |
| 563 | + + debug_vars(x, y) |
| 564 | + ) |
| 565 | + except ZeroDivisionError: |
| 566 | + raise ValidationError( |
| 567 | + _('Cannot perform modulo operation with divisor of zero') + debug_vars(x, y) |
| 568 | + ) |
| 569 | + |
| 570 | + return cast_to_type(result, cast) |
464 | 571 |
|
465 | 572 |
|
466 | 573 | @register.simple_tag |
|
0 commit comments