Introduction
Once you write mathematical or programming expressions, it is best practice to use integers or fractions for any numbers in the expression, ensuring precision, readability, and compatibility across systems. This approach minimizes rounding errors, improves exact calculations, and aligns with standards in various fields such as engineering, finance, and computer science Small thing, real impact..
Why Use Integers or Fractions?
The precision advantage of integers
Integers are whole numbers without fractional parts (…, -2, -1, 0, 1, 2, …). Because they represent exact values, use integers or fractions for any numbers in the expression when the context demands exactness. Here's one way to look at it: counting items, measuring discrete quantities, or indexing arrays are naturally integer‑based tasks. Using an integer avoids the inherent approximation of decimal representation, which can accumulate errors in large‑scale computations Practical, not theoretical..
Fractions provide exact rational values
Fractions (rational numbers) such as 1/3, 22/7, or -5/4 represent numbers exactly, even when the decimal expansion is infinite. Use integers or fractions for any numbers in the expression when the calculation involves ratios, proportions, or measurements that are not whole numbers but still require exact representation. Fractions prevent loss of precision that occurs with floating‑point decimals, especially in scientific and financial contexts Most people skip this — try not to. But it adds up..
Compatibility and readability
Both integers and fractions are universally understood across programming languages, calculators, and mathematical notation. In practice, Use integers or fractions for any numbers in the expression to make the code or formula easier for others to read and verify. Decimal numbers with many digits can be confusing, whereas a simple fraction like 3/4 instantly conveys the intended value Simple, but easy to overlook..
Easier said than done, but still worth knowing.
Benefits of Integers
- Exact arithmetic – Operations on integers yield exact results (e.g., 5 + 3 = 8) without hidden rounding.
- Performance – Integer operations are generally faster and require less memory than floating‑point calculations.
- Simplicity – Integer values are straightforward to parse, validate, and debug.
Example: In a loop that iterates over a list of 100 items, using for i = 0; i < 100; i++ (integers) is clearer and more efficient than using a floating‑point counter like i = 0.0; i < 100.0; i += 1.0.
When to Use Fractions
- Ratios and proportions – When expressing a relationship such as “half of a quantity,” write
1/2instead of0.5. - Exact mathematical constants – Values like π can be approximated as a fraction (22/7) for specific calculations, though true π remains irrational.
- Algebraic simplification – Fractions allow cancellation and reduction, leading to cleaner expressions (e.g., 4/6 simplifies to 2/3).
Tip: In many programming languages, a fraction can be represented using a rational data type (e.g., Python’s fractions.Fraction) or by storing numerator and denominator separately.
Practical Examples
Example 1: Financial calculations
When calculating interest without rounding errors, use integers or fractions for any numbers in the expression. For a yearly rate of 5% on a principal of 1000, the interest can be expressed as:
interest = principal * 5 / 100 // uses integer division for exactness
If you used 0.05 (a floating‑point decimal), you might encounter tiny rounding discrepancies over many years.
Example 2: Engineering measurements
A gear ratio of 3:4 is best written as the fraction 3/4. Using a decimal like 0.75 is acceptable, but the fraction retains the exact relationship between teeth counts, which is critical in design validation.
Example 3: Programming loops
Consider a loop that processes every 5th element:
for index = 0; index < size; index = index + 5 // integers only
Using a floating‑point increment (index = index + 5.0) works but adds unnecessary type conversion overhead Easy to understand, harder to ignore..
Common Pitfalls
- Implicit conversion – Some languages automatically convert integers to floats during division, which can introduce unexpected results. Explicitly use integer division (
//in Python) or fraction types to avoid this. - Overflow – Very large integers may exceed the range of standard data types. In such cases, consider using arbitrary‑
precision libraries (e.g., Python’s built-in int, Java’s BigInteger, or C++’s boost::multiprecision::cpp_int) to maintain exactness without overflow.
-
Loss of significance in mixed expressions – Combining integers with floating-point literals often promotes the entire calculation to floating-point, silently discarding the exactness you intended. Adopt a convention of suffixing integer literals (e.g.,
5Lin Java/C#) or using explicit constructors (Fraction(1, 3)) to keep the type domain clear Not complicated — just consistent.. -
Division by zero – While integer division by zero typically throws a runtime exception, fraction implementations may defer the error until the value is accessed. Validate denominators early, especially when they originate from user input or external configuration It's one of those things that adds up..
Best Practices Summary
| Scenario | Recommended Representation | Rationale |
|---|---|---|
| Counting, indexing, discrete steps | Integer (int, long) |
Exact, fast, minimal memory |
| Ratios, rates, slope, gear teeth | Fraction / Rational (Fraction, Ratio) |
Exact cancellation, no rounding drift |
| Financial cents / fixed-point | Scaled Integer (store $19.99 as 1999) |
Deterministic rounding, legal compliance |
| Physical measurements (sensor data) | Floating-point (float, double) |
Native hardware support, dynamic range |
| High-precision constants (π, e, √2) | Symbolic / Arbitrary-precision | Preserves algebraic properties |
When in doubt, start with the most restrictive exact type the domain allows. Widen to floating-point only when the problem genuinely requires continuous approximation—such as rendering graphics, simulating physics, or processing analog signals.
Conclusion
Choosing between integers, fractions, and floating-point numbers is not merely a syntactic preference; it is a design decision that governs correctness, performance, and maintainability. Integers give you speed and absolute precision for discrete domains. Fractions preserve proportional relationships and enable algebraic reasoning that decimals obscure. Floating-point arithmetic remains indispensable for continuous science and engineering, but it demands respect for its inherent imprecision That's the part that actually makes a difference..
By matching the numeric representation to the mathematical nature of the data—discrete vs. continuous, exact vs. approximate—you eliminate entire classes of bugs before they reach production. The next time you declare a variable, ask yourself: Does this value count something, relate two quantities, or measure a continuum? The answer will guide you to the right tool, and your future self (and your users) will thank you for the clarity.
Appendix: Implementation Patterns & Language-Specific Guidance
The theory of numeric representation only pays dividends when it survives contact with a specific runtime. Below are battle-tested patterns for the most common ecosystems.
1. The “Money” Pattern: Scaled Integers Done Right
Never store currency as float or double. Use a dedicated Money value object backed by a long (or BigInteger for crypto/high-volume) representing the minor unit (cents, pence, satoshis).
// Java example (similar in C#, Kotlin, Go)
public final class Money implements Comparable {
private final long amount; // stored in minor units
private final Currency currency;
private Money(long amount, Currency currency) {
this.On the flip side, amount = amount;
this. currency = Objects.
public static Money ofMajor(BigDecimal major, Currency currency) {
// Round HALF_EVEN (Banker's Rounding) is standard for finance
long minor = major.Consider this: multiply(BigDecimal. Think about it: valueOf(100))
. setScale(0, RoundingMode.HALF_EVEN)
.
public Money add(Money other) {
requireSameCurrency(other);
return new Money(Math.amount), currency); // Overflow check
}
// No setters, no default constructor, equals/hashCode based on fields
}
Key rule: The constructor is private. amount, other.In real terms, addExact(this. Because of that, all creation flows through validation factories (ofMajor, ofMinor, parse). This prevents new Money(-50, USD) from slipping in via deserialization frameworks.
2. Fractions: Normalization is Non-Negotiable
A Fraction class that stores 3/6 instead of 1/2 will destroy performance (growing numerators/denominators) and break equals()/hashCode() contracts.
# Python example using math.gcd (available in stdlib)
from math import gcd
from dataclasses import dataclass
@dataclass(frozen=True, order=True)
class Fraction:
num: int
den: int = 1
def __post_init__(self):
if self.In real terms, num)
object. Because of that, den)
# Reduce
g = gcd(self. den)
if g !num // g)
object.__setattr__(self, 'den', -self.num, self.Consider this: __setattr__(self, 'num', -self. __setattr__(self, 'num', self.den < 0:
object.den == 0:
raise ZeroDivisionError("Denominator cannot be zero")
# Normalize sign to numerator
if self.= 1:
object.__setattr__(self, 'den', self.
def __mul__(self, other: 'Fraction') -> 'Fraction':
# Cross-cancel *before* multiplication to delay overflow
n1, d1 = self.num, self.den
n2, d2 = other.In practice, num, other. den
g1 = gcd(n1, d2)
g2 = gcd(n2, d1)
return Fraction((n1 // g1) * (n2 // g2), (d1 // g2) * (d2 // g1))
Cross-cancellation (shown in __mul__) is the single most effective optimization for rational arithmetic, keeping intermediate values small enough for 64-bit integers far longer than naive multiplication Most people skip this — try not to..
3. Floating-Point Hygiene: The ulp Discipline
When you must use `double
When you must use double, never compare values with ==. Instead, compare their ulp (unit in the last place) distance:
public static boolean approximatelyEqual(double a, double b, double ulp) {
return Math.abs(a - b) <= ulp * Math.max(Math.abs(a), Math.abs(b));
}
This gives you a relative tolerance that scales with magnitude—exactly what you need when comparing cents that have been multiplied or divided through repeated operations. Which means for money specifically, if you find yourself in a situation where double is unavoidable (legacy APIs, performance-critical hot paths), treat the result as an approximation boundary and document it explicitly. The discipline of knowing where your precision ends and your tolerance begins is more valuable than any amount of defensive coding around double arithmetic And that's really what it comes down to. And it works..
4. Immutability Is Not Optional—It's Structural
Every class discussed so far is immutable by design. Think about it: this single property eliminates an entire class of bugs: no aliasing, no accidental mutation during iteration, no stale state across threads. In Java, this means fields are final; in Python, @dataclass(frozen=True); in C++, const members with deleted setters The details matter here..
The cost is allocation overhead. But if profiling reveals that immutability is creating GC pressure in a hot path, consider object pooling for short-lived instances—but only as an optimization after the immutable design has proven correct. Premature pooling introduces mutable shared state, silently reintroducing the bugs you just eliminated.
5. Validation Belongs at the Boundary
The constructor enforces invariants. The factory methods (ofMajor, ofMinor, parse) are the only entry points. Everything else—JSON deserialization, database mapping, method chaining—flows through these validated factories.
// Bad: deserialization can bypass validation
Money m = Money.fromJson(jsonString); // hypothetical
// Good: parse goes through ofMajor/ofMinor, which validate
Money m = Money.parse("99.99", Currency.
In Java, this means using a custom `@JsonCreator` that delegates to your factory. In practice, in Python, a `@classmethod` that wraps `ofMajor`. The principle is identical regardless of language: **the creation path is the validation path.
---
## Conclusion
Building a financial type is not about choosing between simplicity and correctness. It is about recognizing that the domain of monetary values imposes constraints—strict representation, normalization, overflow protection, and immutability—that are non-negotiable for any code that touches real money.
The lessons above are not exotic techniques. They are the same principles that make `BigDecimal` reliable in Java, that make Python's `Fraction` performant, and that make every serious financial library in existence safe. If you apply them consistently, you will ship code that behaves correctly whether it processes a single transaction or billions of them.
The alternative—relying on `double` or `float`, skipping normalization, allowing mutation—is not a shortcut. Because of that, it is a bet against your own requirements. And in financial software, that bet does not pay off.