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" /> <img src="docs/images/demo-rounding.png" width="30%" alt="AZDecimal — rounding mode comparison" /> <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_PRECISIONis defined with a#ifndefguard inSources/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" // 4Rounding
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/AZCalcOr 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