Numeric Types and Precision Reference
TerminusDB provides precise control over numeric types and arithmetic operations, following W3C XSD standards and Prolog's natural semantics. This guide explains how numeric types work, when precision is preserved, and how to choose the right type for your use case.
--> Valid as of the 11.2 release.
Numeric Type Overview
TerminusDB supports four primary numeric types for storage, and performs arithmetic operations using two families of xsd:decimal and xsd:double:
| Type | Storage | Precision | Use Case |
|---|---|---|---|
xsd:decimal | Rational number | Exact (arbitrary size and precision, capped at 20 decimals) | Financial calculations, exact arithmetic |
xsd:integer | Rational number (subset) | Exact (arbitrary size) | Counting, indexing, exact integers |
xsd:double | IEEE 754 64-bit float | Approximate (15-17 digits) | Scientific calculations, measurements |
xsd:float | IEEE 754 32-bit float | Approximate (6-9 digits) | Low-precision measurements |
Type Inference Rules
When performing arithmetic operations, TerminusDB follows Prolog's natural semantics, where floating point numbers are "contagious."
Pure Type Operations during arithmetic
| Operation | Result Type | Example |
|---|---|---|
xsd:decimal + xsd:decimal | xsd:decimal | Exact: 0.1 + 0.2 = 0.3 |
xsd:integer + xsd:integer | xsd:decimal | Exact: 5 + 3 = 8 |
xsd:double + xsd:double | xsd:double | IEEE 754: 0.1 + 0.2 = 0.30000000000000004 |
xsd:float + xsd:float | xsd:double | Promoted to double precision |
Mixed Type Operations (Floats Are Contagious)
Rule: If ANY operand is xsd:double or xsd:float, the result is xsd:double.
| Operation | Result Type | Rationale |
|---|---|---|
xsd:double + xsd:decimal | xsd:double | Float "infects" the operation |
xsd:double + xsd:integer | xsd:double | Float takes precedence |
xsd:float + xsd:decimal | xsd:double | Float promoted to double |
Why are floats contagious?
This follows Prolog's arithmetic semantics, ensuring consistent and predictable behavior. When you mix approximate (float) with exact (rational) types, the result must be approximate since the float has already lost precision during parsing.
Division Operations
Division behavior depends on operand types:
| Operands | Operator Used | Result |
|---|---|---|
Both xsd:decimal or xsd:integer | rdiv (rational division) | Exact: 1/3 stays as 1 rdiv 3 during evaluation, stored as xsd:decimal |
Any xsd:double or xsd:float | / (IEEE 754 division) | IEEE 754: 1.0/3.0 = 0.3333333333333333 |
Examples
Looking at the pure WOQL AST helps show the evaluation structure.
// Rational division (exact)
{
"@type": "Eval",
"expression": {
"@type": "Divide",
"left": { "@type": "ArithmeticValue", "data": { "@type": "xsd:decimal", "@value": "1" } },
"right": { "@type": "ArithmeticValue", "data": { "@type": "xsd:decimal", "@value": "3" } }
},
"result": { "@type": "ArithmeticValue", "variable": "Result" }
}
// Result: { "@type": "xsd:decimal", "@value": "0.3333333..." } (processed as 1/3 rational, stored as decimal if persisted)
// IEEE 754 division (approximate)
{
"@type": "Eval",
"expression": {
"@type": "Divide",
"left": { "@type": "ArithmeticValue", "data": { "@type": "xsd:double", "@value": "1.0" } },
"right": { "@type": "ArithmeticValue", "data": { "@type": "xsd:double", "@value": "3.0" } }
},
"result": { "@type": "ArithmeticValue", "variable": "Result" }
}
// Result: { "@type": "xsd:double", "@value": 0.3333333333333333 }
Precision Loss: When and Why
Critical Understanding
Precision is lost at parse time, not during arithmetic!
Example: The 0.1 + 0.2 Problem
// Using xsd:double (precision lost at parse time)
{
"@type": "Eval",
"expression": {
"@type": "Plus",
"left": { "@type": "ArithmeticValue", "data": { "@type": "xsd:double", "@value": "0.1" } },
"right": { "@type": "ArithmeticValue", "data": { "@type": "xsd:double", "@value": "0.2" } }
},
"result": { "@type": "ArithmeticValue", "variable": "Result" }
}
// Result: { "@type": "xsd:double", "@value": 0.30000000000000004 }
// Why? "0.1" cannot be represented exactly in binary floating point!
// Using xsd:decimal (exact)
{
"@type": "Eval",
"expression": {
"@type": "Plus",
"left": { "@type": "ArithmeticValue", "data": { "@type": "xsd:decimal", "@value": "0.1" } },
"right": { "@type": "ArithmeticValue", "data": { "@type": "xsd:decimal", "@value": "0.2" } }
},
"result": { "@type": "ArithmeticValue", "variable": "Result" }
}
// Result: { "@type": "xsd:decimal", "@value": "0.3" } (exact!)
Why Mixing Doesn't Help
// Mixed types: precision ALREADY lost
{
"@type": "Eval",
"expression": {
"@type": "Plus",
"left": { "@type": "ArithmeticValue", "data": { "@type": "xsd:double", "@value": "0.1" } },
"right": { "@type": "ArithmeticValue", "data": { "@type": "xsd:decimal", "@value": "0.2" } }
},
"result": { "@type": "ArithmeticValue", "variable": "Result" }
}
// Result: { "@type": "xsd:double", "@value": 0.30000000000000004 }
// Why? The xsd:double(0.1) was already imprecise before arithmetic!
Best Practices
Use xsd:decimal When
- Financial calculations (money, prices, invoices)
- Exact arithmetic required (inventory, quantities)
- Precision matters more than performance
- Regulatory compliance required
// Good: Financial calculation, make sure to handle JSON correctly
// Be especially careful with javascript, you can always send as strings
// Ensure to use the "reviver" pattern and leverage Decimal.js or equivalent!
{
price: { "@type": "xsd:decimal", "@value": "99.99" },
tax: { "@type": "xsd:decimal", "@value": "0.08" },
total: { "@type": "xsd:decimal", "@value": "107.99" }
}
Use xsd:double When
- Scientific measurements
- Statistical calculations
- Approximate values (sensor readings)
- Performance-critical operations
- Interoperability with IEEE 754 systems
// Good: Scientific measurement
{
temperature: { "@type": "xsd:double", "@value": 98.6 },
coordinates: {
latitude: { "@type": "xsd:double", "@value": 51.5074 },
longitude: { "@type": "xsd:double", "@value": -0.1278 }
}
}
Use xsd:integer When
- Counting (items, users, events)
- IDs and identifiers
- Array indices
- Exact whole numbers
// Good: Counting
{
quantity: { "@type": "xsd:integer", "@value": "42" },
userId: { "@type": "xsd:integer", "@value": "12345" }
}
Variable Arithmetic
Variables preserve their types during arithmetic:
// Variables maintain their types
{
"@type": "And",
"and": [
{
"@type": "Equals",
"left": { "variable": "x" },
"right": { "data": { "@type": "xsd:double", "@value": "0.1" } }
},
{
"@type": "Equals",
"left": { "variable": "y" },
"right": { "data": { "@type": "xsd:double", "@value": "0.2" } }
},
{
"@type": "Eval",
"expression": {
"@type": "Plus",
"left": { "variable": "x" },
"right": { "variable": "y" }
},
"result": { "variable": "result" }
}
]
}
// Result: { "@type": "xsd:double", "@value": 0.30000000000000004 }
// The variables x and y are xsd:double, so result is xsd:double
Common Pitfalls
Pitfall 1: Assuming Mixed Types Preserve Precision
❌ Wrong Assumption:
// "If I add xsd:double to xsd:decimal, I'll get exact result"
xsd:double(0.1) + xsd:decimal(0.2) // Hoping for 0.3
✅ Reality:
// Result is 0.30000000000000004 (xsd:double)
// Precision was ALREADY lost when parsing "0.1" as xsd:double
Pitfall 2: Using xsd:double for Money
❌ Wrong:
{
price: { "@type": "xsd:double", "@value": "19.99" },
tax: { "@type": "xsd:double", "@value": "0.08" }
}
// Can lead to rounding errors in financial calculations!
✅ Right:
{
price: { "@type": "xsd:decimal", "@value": "19.99" },
tax: { "@type": "xsd:decimal", "@value": "0.08" }
}
Pitfall 3: Comparing Floats for Equality
❌ Problematic:
0.1 + 0.2 == 0.3 // false with xsd:double!
✅ Better:
// Use xsd:decimal for exact comparisons
0.1 + 0.2 == 0.3 // true with xsd:decimal!
Technical Details
Internal Representation
| Type | Prolog Processing | Storage | Example |
|---|---|---|---|
xsd:decimal | Rational (numerator rdiv denominator) | TerminusDB Storage: 0.3 | 3r10 (represents 0.3) |
xsd:decimal | Rational (denominator = 1) | TerminusDB Storage: 42 (not 42.0) | 42 |
xsd:integer | Rational (denominator = 1) | TerminusDB Storage: 42 | 42 |
xsd:double | IEEE 754 float (64 bit) | TerminusDB Storage: 0.30000000000000004 (64 bit) | 0.30000000000000004 |
Type Checking at Runtime
TerminusDB uses Prolog's type predicates to determine result types:
% Type inference (simplified)
infer_result_type(Value, 'xsd:decimal') :-
rational(Value), % Check if it's a rational number
!.
infer_result_type(_Value, 'xsd:double'). % Otherwise it's a float
Comparing Different Types
When comparing different types, use typecast to convert one or both operands to the same type family (xsd:decimal, xsd:integer, xsd:double or xsd:float).
Performance Considerations
| Aspect | xsd:decimal | xsd:double |
|---|---|---|
| Storage | Variable memory (rational representation) | 8 bytes fixed |
| Arithmetic Speed | Slower (rational arithmetic) | Faster (hardware floats) |
| Precision | ✅ Exact (arbitrary precision, capped at 20 decimals) | ⚠️ Approximate (15-17 digits precision) |
| Range | ✅ Unlimited | ±1.7 × 10^308 |
When Performance Matters
For performance-critical operations with millions of calculations:
- Use
xsd:doubleif approximate results are acceptable - Use
xsd:decimalif precision is non-negotiable - Consider pre-rounding to reduce decimal precision when possible
Migration Guide
Converting Existing Data
// From xsd:double to xsd:decimal (be aware of precision loss)
{
"@type": "Cast",
"value": { "@type": "xsd:double", "@value": 0.30000000000000004 },
"type": "xsd:decimal"
}
// Result: Best-fit decimal representation
// From xsd:decimal to xsd:double (safe but loses exactness)
{
"@type": "Cast",
"value": { "@type": "xsd:decimal", "@value": "0.3" },
"type": "xsd:double"
}
// Result: 0.3 (may not be exact due to IEEE 754)
Summary
Key Takeaways
- Use
xsd:decimalfor exact arithmetic (especially money) - Floats are contagious - any float makes the result a float
- Precision is lost at parse time, not during arithmetic
- Division uses
rdivfor rationals,/for floats - Follow Prolog's natural semantics - consistent and predictable
See Also
- Supported Data Types - Complete list of XSD types
- WOQL Math Queries - Arithmetic operations in WOQL
- Schema Reference Guide - Defining numeric properties in schemas