Contents

sumpositive/azcalc

BCD decimal arithmetic and formula evaluation for Swift.

Demo

<p> <img src="docs/images/demo-formula.png" width="30%" alt="AZFormula — formula evaluation with √2" /> &nbsp; <img src="docs/images/demo-rounding.png" width="30%" alt="AZDecimal — rounding mode comparison" /> &nbsp; <img src="docs/images/demo-format.png" width="30%" alt="AZDecimal — number formatting" /> </p>


AZDecimal

Floating-point-free arithmetic using Binary Coded Decimal (BCD). Up to 30 integer digits + 30 decimal digits (60 digits total).

Changing precision: SBCD_PRECISION is defined with a #ifndef guard in Sources/AZDecimalC/include/SBCD.h. When forking the package, you can override it via a build flag (-DSBCD_PRECISION=120). The value must be even. The upper bound is limited by stack consumption of the fixed-size C array (char digit[SBCD_PRECISION+1]).

Basic usage

import AZDecimal

let a: AZDecimal = "1.1"
let b: AZDecimal = "2.0456"
print(a + b)   // "3.1456"
print(a - b)   // "-0.9456"
print(a * b)   // "2.25016"
print(a / b)   // "0.537815..."

Convenience API

AZDecimal.zero          // AZDecimal("0")
AZDecimal.one           // AZDecimal("1")

let x: AZDecimal = "-3.14"
x.isZero                // false
x.isNegative            // true
x.abs                   // AZDecimal("3.14")

var a: AZDecimal = "10"
a += "3"   // 13
a -= "5"   // 8
a *= "2"   // 16
a /= "4"   // 4

Rounding

let config = AZDecimalConfig(decimalDigits: 2, roundType: .r54)
let result = AZDecimal("3.456").rounded(config)
print(result)  // "3.46"

Rounding modes

| Mode | Name | Description | Standard | |---|---|---|---| | .rup | Round Up | Round away from zero | — | | .rPlus | Round toward +∞ | Round toward positive infinity | IEEE 754: roundTowardPositive | | .r54 | Round Half Up | Round half up | JIS Z 8401 Rule B | | .r55 | Round Half Even | Round half to even — Banker's rounding | JIS Z 8401 Rule A · IEEE 754: roundTiesToEven | | .r65 | Round Half Down | 5 rounds down, 6+ rounds up | — | | .rMinus | Round toward −∞ | Round toward negative infinity | IEEE 754: roundTowardNegative | | .truncate | Truncate | No rounding — return raw value | IEEE 754: roundTowardZero |

Formatting

formatted(:) applies grouping separators, decimal separator, and trailing-zero padding. It also truncates the decimal part to decimalDigits. Call rounded(:) first for precise rounding.

let config = AZDecimalConfig.default
    .digits(2)
    .rounding(.r54)
    .trailingZero(true)
    .grouping(.threes)

let value = AZDecimal("1234567.045")
print(value.rounded(config).formatted(config))
// "1,234,567.05"

Fluent configuration

AZDecimalConfig supports method chaining. The default config uses .r54, 3 decimal digits, 3-digit grouping, no trailing zeros.

// verbose style
var config = AZDecimalConfig()
config.decimalDigits = 2
config.roundType = .r54

// fluent style (equivalent)
let config = AZDecimalConfig.default.digits(2).rounding(.r54)

| Method | Description | |---|---| | .digits( n: Int) | Set decimal digits | | .rounding( type: RoundType) | Set rounding mode | | .trailingZero( enabled: Bool) | Pad / strip trailing zeros | | .grouping( type: GroupType, separator: String) | Set digit grouping | | .decimalSep(_ separator: String) | Set decimal separator |

Grouping types

| Type | Example | |---|---| | .none | 1234567 | | .threes | 1,234,567 | | .fours | 123,4567 | | .indian | 12,34,567 |


AZFormula

Evaluates infix formula strings using the Shunting Yard algorithm.

Basic usage

import AZFormula

let result = AZFormula.evaluate("(100 + 5%) × 1.08")
// → .success("113.4")

switch AZFormula.evaluate("1 ÷ 3") {
case .success(let value): print(value)  // "0.333"  (default: .r54, 3 digits)
case .failure(let error): print(error)
}

With rounding config

let config = AZDecimalConfig.default.digits(2).rounding(.r54)

let result = AZFormula.evaluate("1 ÷ 3", config: config)
// → .success("0.33")

evaluateDecimal — returns AZDecimal

When you need to perform further arithmetic on the result:

if case .success(let a) = AZFormula.evaluateDecimal("10+5"),
   case .success(let b) = AZFormula.evaluateDecimal("2+1") {
    print(a * b)  // AZDecimal("45")
}

Supported operators

| Operator | Description | |---|---| | + - × ÷ | Basic arithmetic (* / also accepted) | | | Square root — BCD Newton-Raphson, full precision | | | Cube root — BCD Newton-Raphson, full precision | | ( ) | Parentheses | | % | Percent — context-sensitive (see below) | | | Japanese percent notation |

Percent operator behavior

| Expression | Expands to | Result | |---|---|---| | 100+5% | 100×(100+5)÷100 | 105 | | 100-5% | 100×(100-5)÷100 | 95 | | 100×5% | 100×5÷100 | 5 | | 100÷5% | 100÷5×100 | 2000 |

Error cases

public enum AZFormulaError: Error {
    case tooLong          // formula exceeds AZFormula.maxFormulaLength characters (default: 200, settable at runtime)
    case negativeSqrt     // √ applied to a negative number
    case invalidExpression
}

Sub-step API (for testing / custom pipelines)

let tokens = AZFormula.tokenize("100+5%")
// ["100", "×", "(", "100", "+", "5", ")", "÷", "100"]

let rpn = AZFormula.toRPN(tokens)
// ["100", "100", "5", "+", "×", "100", "÷"]

Installation

Swift Package Manager

In Xcode: File → Add Package Dependencies

https://github.com/SumPositive/AZCalc

Or add to your Package.swift:

dependencies: [
    .package(url: "https://github.com/SumPositive/AZCalc", from: "1.0.0")
]

Add products to your target:

.target(
    name: "MyApp",
    dependencies: [
        .product(name: "AZDecimal", package: "AZCalc"),
        .product(name: "AZFormula", package: "AZCalc"),
    ]
)

Requirements

  • iOS 16.0+ / macOS 13.0+
  • Swift 5.9+
  • Xcode 15+

Development

AZCalc/
├── Package.swift
├── Sources/
   ├── AZDecimalC/ BCD arithmetic core (C++)
   ├── AZDecimal/ Swift API
   └── AZFormula/ Formula engine
├── Tests/
   ├── AZDecimalTests/ 51 tests (arithmetic, rounding, comparable, convenience, format, root)
   └── AZFormulaTests/ 46 tests (tokenize, RPN, evaluate, evaluateDecimal)
└── Demo/
    └── AZCalcDemo.xcodeproj  SwiftUI demo app (iOS 17+)

Open AZCalc.xcworkspace in Xcode to run both package tests and demo app tests together.

License

MIT License. See LICENSE for details.

Package Metadata

Repository: sumpositive/azcalc

Default branch: main

README: README.md