# NumberF Cheatsheet

Every NumberF function on one page. All examples are real output.

## Installation
{: .col-2}

### mix.exs

```elixir
def deps do
  [{:number_f, "~> 0.3.0"}]
end
```

### Usage

No configuration, no setup, no supervision tree
entry. Call everything off `NumberF`:

```elixir
NumberF.currency(1234.5)
# => "ZMW 1,234.50"
```

## Currency & number formatting
{: .col-2}

### Currency

```elixir
NumberF.currency(1234.567)
# => "ZMW 1,234.57"

NumberF.currency(1234.567, "USD")
# => "USD 1,234.57"

NumberF.currency(1234.567, "USD", 0)
# => "USD 1,235"
```

### Delimiters

```elixir
NumberF.comma_separated(1234567.89)
# => "1,234,567.89"

NumberF.number_to_delimited(1234567.89,
  delimiter: ".", separator: ",")
# => "1.234.567,89"
```

### Abbreviation

```elixir
NumberF.abbreviate_number(1_234_567)
# => "1.2M"

NumberF.abbreviate_number(1_234_567, 2)
# => "1.23M"

NumberF.abbreviate_number(1_500_000_000_000)
# => "1.5T"

NumberF.abbreviate_number(-5000)
# => "-5.0K"
```

### Percentages

```elixir
NumberF.percentage(25, 200)
# => 12.5

NumberF.percentage(25, 200, 2)
# => 12.5
```

## Numbers to text
{: .col-2}

### Words (currency style)

```elixir
NumberF.to_words(42)
# => "Forty Two Kwacha"

NumberF.to_words(42.75, "Dollars", "Cents")
# => "Forty Two Dollars And Seventy Five Cents"
```

### Words (multilingual)

```elixir
NumberF.spell_number(42, "en")
# => "Forty-two"

NumberF.spell_number(42, "fr")
# => "Quarante-deux"

# Fractions are read digit by digit, so a
# leading zero survives:
NumberF.spell_number(1.05, "en")
# => "One point zero five"
```

`NumberF.spelling_languages()` => `["de", "en",
"es", "fr"]`. Any other language raises
`ArgumentError` rather than quietly returning
English.

### Ordinals

```elixir
NumberF.ordinal(1)    # => "1st"
NumberF.ordinal(21)   # => "21st"
NumberF.ordinal(112)  # => "112th"
```

### Roman numerals

```elixir
NumberF.to_roman(1999)     # => "MCMXCIX"
NumberF.from_roman("MCMXCIX")  # => 1999
```

## Internationalization
{: .col-2}

### Locale-aware numbers

```elixir
NumberF.format_number(1234567.89, "en-US")
# => "1,234,567.89"

NumberF.format_number(1234567.89, "fr-FR")
# => "1 234 567,89"

NumberF.format_number(1234567.89, "de-DE")
# => "1.234.567,89"
```

### Locale-aware currency

```elixir
NumberF.format_currency(1234.56, "en-US")
# => "$1,234.56"

NumberF.format_currency(1234.56, "fr-FR")
# => "1 234,56 €"

NumberF.format_currency(1234.56, "de-DE",
  currency_code: "USD")
# => "1.234,56 $"
```

### Currency metadata

```elixir
NumberF.get_currency("USD")
# => %{name: "US Dollar", symbol: "$", ...}

NumberF.currency_data()
# => %{"USD" => %{symbol: "$", ...}, ...}
```

### Explicit currency codes

```elixir
NumberF.format_with_currency(1234.56, "JPY")
# => "¥1,235"    (JPY has 0 decimal places)
```

## Financial calculations
{: .col-2}

### Interest

```elixir
NumberF.simple_interest(1000, 0.05, 2)
# => 100.0

NumberF.compound_interest(1000, 0.05, 2)
# => 102.5

# 12 compounding periods per year
NumberF.compound_interest(1000, 0.05, 2, 12)
# => 104.94
```

### Loan repayment

```elixir
# principal, annual rate, months
NumberF.calculate_emi(100_000, 0.10, 12)
# => 8791.59
```

### Currency conversion

```elixir
# amount, from_rate, to_rate
NumberF.convert_currency(100, 1.0, 0.85)
# => 85.0
```

### Decimal-safe sums

```elixir
NumberF.sum_decimal([
  Decimal.new("1.10"),
  Decimal.new("2.20")
])
# => Decimal.new("3.30")
```

## Tax
{: .col-2}

### VAT

```elixir
NumberF.calculate_vat(100, 0.2)
# => %{net: 100.0, vat: 20.0, gross: 120.0}

# VAT-inclusive: extract tax from gross
NumberF.calculate_vat(120, 0.2, true)
# => %{net: 100.0, vat: 20.0, gross: 120.0}
```

### Sales tax

```elixir
NumberF.calculate_sales_tax(100, 0.06)
# => %{subtotal: 100.0, tax: 6.0, total: 106.0}

NumberF.calculate_sales_tax(100, 0.0625, round_to: 0.05)
# => %{subtotal: 100.0, tax: 6.25, total: 106.25}
```

### Income tax (progressive)

```elixir
brackets = NumberF.Tax.income_tax_brackets()["US"]

NumberF.Tax.calculate_income_tax(75_000, brackets)
# => %{tax: 12248.5, effective_rate: 0.1633}
```

### Other taxes

```elixir
NumberF.Tax.calculate_capital_gains_tax(50_000, 0.15)
# => %{gain: 50000.0, taxable_gain: 50000.0,
#      tax: 7500.0, net: 42500.0}

NumberF.Tax.calculate_corporate_tax(500_000, 0.21)
NumberF.Tax.calculate_withholding_tax(1000, 0.15)
NumberF.Tax.calculate_payroll_tax(60_000, 0.062, 0.0145)

NumberF.vat_rates()          # VAT rate by country
NumberF.Tax.income_tax_brackets()
# => %{"US" => [...], "UK" => [...],
#      "Germany" => [...], "Zambia" => [...]}
```

## Statistics
{: .col-2}

### Central tendency

```elixir
NumberF.mean([1, 2, 3, 4, 5])      # => 3.0
NumberF.median([1, 3, 5, 7, 9])    # => 5
NumberF.mode([1, 2, 2, 3, 3, 3])   # => [3]
```

### Spread

`variance/1` and `standard_deviation/1` are the
**population** forms (÷N). Use the `sample_*`
variants (÷N-1) when the list is a sample —
which is usually what you want.

```elixir
NumberF.variance([1, 2, 3, 4, 5])
# => 2.0

NumberF.standard_deviation([2,4,4,4,5,5,7,9])
# => 2.0

NumberF.sample_variance([2,4,4,4,5,5,7,9])
# => 4.571428571428571

NumberF.sample_standard_deviation([2,4,4,4,5,5,7,9])
# => 2.138089935299395

NumberF.range([1, 5, 9])   # => 8.0
```

## Precision & rounding
{: .col-2}

### Bankers rounding (round half to even)

```elixir
NumberF.bankers_round(2.5, 0)   # => 2.0
NumberF.bankers_round(3.5, 0)   # => 4.0
```

Use this for financial totals — it avoids the
upward bias of always rounding `.5` away from zero.

### Directional rounding

`ceiling/2` and `floor/2` take **decimal places**;
`round_to/3` takes an **increment**.

```elixir
NumberF.ceiling(2.111, 2)         # => 2.12
NumberF.floor(2.999, 2)           # => 2.99

NumberF.round_to(127, 5)          # => 125.0
NumberF.round_to(2.567, 0.05)     # => 2.55
NumberF.round_to(2.5, 1, :up)     # => 3.0

NumberF.round_to_nearest(127, 5)  # => 125.0
```

### Guards

```elixir
NumberF.clamp(15, 0, 10)         # => 10
NumberF.safe_divide(10, 0)       # => 0.0
NumberF.safe_divide(10, 0, :none) # => :none
NumberF.sign(-4.2)               # => -1
```

### Float comparison

```elixir
NumberF.approximately_equal(0.1 + 0.2, 0.3)
# => true

NumberF.Precision.sanitize_float(:nan)
```

Never compare floats with `==`. Use
`approximately_equal/3` with an explicit epsilon.

### Precision helpers

```elixir
NumberF.round_with_precision(3.14159, 2)
# => 3.14

NumberF.Precision.custom_round(2.565, 2, :half_down)
# => 2.56

NumberF.Precision.truncate(3.999, 2)
# => 3.99

NumberF.Precision.precise_format(3.14159, 3)
# => "3.142"
```

## Validation
{: .col-2}

### Credit cards (Luhn)

```elixir
NumberF.is_valid_credit_card?("4111111111111111")
# => true

NumberF.is_valid_credit_card?("4111-1111-1111-1111")
# => true
```

### Number formats

```elixir
NumberF.is_valid_number?("1234.56")   # => true
NumberF.is_valid_integer?("1234")     # => true
NumberF.in_range?(5, 1, 10)           # => true
```

## Unit conversion
{: .col-2}

### Temperature

```elixir
NumberF.celsius_to_fahrenheit(25)   # => 77.0
NumberF.fahrenheit_to_celsius(77)   # => 25.0
```

### Length & distance

```elixir
NumberF.km_to_miles(100)
NumberF.miles_to_km(62)
NumberF.cm_to_inches(30)
NumberF.inches_to_cm(12)
```

### Weight & volume

```elixir
NumberF.kg_to_pounds(70)
NumberF.Metrics.pounds_to_kg(154)
NumberF.Metrics.ml_to_oz(500)
NumberF.Metrics.oz_to_ml(16)
```

### Generic conversion

Unit keys are strings, not atoms.

```elixir
NumberF.Metrics.convert_units(100, "km", "mi",
  NumberF.Metrics.length_units())
# => 62.137...
```

Unit tables: `length_units/0`, `weight_units/0`,
`volume_units/0`, `area_units/0`.

## Dates
{: .col-2}

### Age & spans

```elixir
NumberF.calculate_age(~D[1990-01-15])
NumberF.days_between(~D[2025-01-01], ~D[2025-12-31])
```

### Business days

```elixir
NumberF.is_business_day?(~D[2025-06-14])
NumberF.business_days_between(
  ~D[2025-01-01], ~D[2025-01-31])
```

### Payment terms

```elixir
NumberF.payment_due_date(~D[2025-01-01])
NumberF.payment_due_date(~D[2025-01-01], 60)
```

## Advanced statistics
{: .col-2}

### Percentiles and spread

```elixir
NumberF.percentile([1,2,3,4,5,6,7,8,9,10], 25)
# => 3.25

NumberF.quartiles([1,2,3,4,5,6,7,8])
# => %{q1: 2.75, q2: 4.5, q3: 6.25}

NumberF.iqr([1,2,3,4,5,6,7,8])
# => 3.5
```

### Relationships between two series

```elixir
NumberF.correlation([1,2,3,4,5], [2,4,5,4,5])
# => 0.7745966692414833

NumberF.covariance([1,2,3,4,5], [2,4,5,4,5])
# => 1.2

NumberF.linear_regression([1,2,3,4,5], [2,4,5,4,5])
# => %{slope: 0.6, intercept: 2.2,
#      r_squared: 0.5999999999999999}
```

### Other means

```elixir
NumberF.weighted_mean([90,80,70], [0.5,0.3,0.2])
# => 83.0

# Growth rates: pass factors, not percentages
NumberF.geometric_mean([1, 4, 16])
# => 4.0

# Rates over a fixed distance
NumberF.harmonic_mean([40, 60])
# => 47.99999999999999  (48, modulo IEEE 754)
```

### Describing and reshaping

```elixir
NumberF.summary([1,2,3,4,5])
# => %{count: 5, min: 1, q1: 2.0, median: 3,
#      q3: 4.0, max: 5, mean: 3.0, stddev: 1.414...}

NumberF.z_scores([2, 4, 6])
# => [-1.2247448713915889, 0.0, 1.2247448713915889]

NumberF.normalize([10, 20, 30])   # => [0.0, 0.5, 1.0]
NumberF.moving_average([1,2,3,4,5], 3) # => [2.0, 3.0, 4.0]
NumberF.cumulative_sum([1,2,3,4])      # => [1, 3, 6, 10]
NumberF.outliers([1,2,3,4,1000])       # => [1000]
NumberF.trimmed_mean([1,2,3,4,100], 0.2) # => 3.0
```

## Investment and loans
{: .col-2}

### Time value of money

```elixir
NumberF.present_value(1000, 0.05, 10)  # => 613.91
NumberF.future_value(1000, 0.05, 10)   # => 1628.89

NumberF.npv(0.1, [-1000, 300, 400, 500, 600])
# => 388.77

NumberF.irr([-1000, 300, 400, 500, 600])
# => {:ok, 0.24888...}
```

`irr/2` returns `{:error, :no_sign_change}` when the
cash flows never cross zero — there is no rate that
zeroes an all-positive series.

### Amortization

```elixir
NumberF.amortization_schedule(100_000, 0.10, 12)
# => [%{period: 1, payment: 8791.59,
#       principal: 7958.26, interest: 833.33,
#       balance: 92041.74}, ...]
```

The last period absorbs accumulated rounding, so the
balance closes at exactly `0.0`.

### Returns and break-even

```elixir
NumberF.roi(1500, 1000)              # => 50.0
NumberF.cagr(1000, 2000, 5)          # => 14.87
NumberF.effective_annual_rate(0.12, 12) # => 0.1268
NumberF.break_even_point(10_000, 25, 15) # => 1000.0
NumberF.payback_period(1000, [300,400,500])
# => {:ok, 2.6}
```

### Annuities and depreciation

```elixir
NumberF.annuity_payment(10_000, 0.05, 10)    # => 1295.05
NumberF.annuity_present_value(100, 0.05, 10) # => 772.17
NumberF.annuity_future_value(100, 0.05, 10)  # => 1257.79

NumberF.depreciation_straight_line(10_000, 1_000, 5)
# => 1800.0

NumberF.depreciation_declining_balance(10_000, 1_000, 5)
# => [%{year: 1, depreciation: 4000.0,
#       book_value: 6000.0}, ...]
```

## Identifiers and checksums
{: .col-2}

### Financial identifiers

```elixir
NumberF.valid_iban?("GB82 WEST 1234 5698 7654 32")
# => true

NumberF.valid_routing_number?("021000021")  # => true
NumberF.valid_currency_code?("USD")         # => true
```

### Cards

```elixir
NumberF.card_brand("4111111111111111")  # => :visa
NumberF.card_brand("5500000000000004")  # => :mastercard
NumberF.card_brand("378282246310005")   # => :amex

NumberF.valid_luhn?("4111111111111111")     # => true
NumberF.luhn_check_digit("411111111111111") # => 1
```

`card_brand/1` identifies the network from the prefix
and length. It does **not** validate — pair it with
`is_valid_credit_card?/1`.

### Products and publications

```elixir
NumberF.valid_isbn?("978-3-16-148410-0")  # => true
NumberF.valid_isbn?("0-306-40615-2")      # => true  (ISBN-10)
NumberF.valid_ean?("4006381333931")       # => true
NumberF.valid_upc?("036000291452")        # => true
NumberF.valid_imei?("490154203237518")    # => true
```

## Number theory and bases
{: .col-2}

### Sequences and factors

```elixir
NumberF.fibonacci(10)            # => 55
NumberF.fibonacci_sequence(8)    # => [0,1,1,2,3,5,8,13]
NumberF.permutations(5, 2)       # => 20
NumberF.prime_factors(360)       # => [2,2,2,3,3,5]
NumberF.divisors(28)             # => [1,2,4,7,14,28]
```

`fibonacci/1` and `combinations/2` return exact
integers at any size — `fibonacci(100)` is the true
value, not a float approximation.

### Digits

```elixir
NumberF.digits(12345)         # => [1,2,3,4,5]
NumberF.digit_sum(12345)      # => 15
NumberF.digital_root(12345)   # => 6
NumberF.reverse_number(12345) # => 54321
NumberF.palindrome?(12321)    # => true
NumberF.perfect_square?(144)  # => true
```

### Roots and logs

```elixir
NumberF.nth_root(27, 3)     # => 3.0
NumberF.log_base(1024, 2)   # => 10.0
```

### Base conversion

```elixir
NumberF.to_base(255, 16)     # => "FF"
NumberF.from_base("FF", 16)  # => 255

NumberF.to_hex(255)      # => "FF"
NumberF.to_binary(5)     # => "101"
NumberF.to_octal(8)      # => "10"
NumberF.from_binary("101")  # => 5
```

Any base from 2 to 36.

## Presentation
{: .col-2}

### Accounting and signs

```elixir
NumberF.accounting_format(-1234.56, unit: "$")
# => "($1,234.56)"

NumberF.with_sign(42)             # => "+42"
NumberF.with_sign(-42)            # => "-42"
NumberF.pad_number(42, 6, char: "0")  # => "000042"
NumberF.pluralize(3, "person", "people")
# => "3 people"
```

### Sizes and durations

```elixir
NumberF.format_bytes(1_500_000)          # => "1.5 MB"
NumberF.format_bytes(1_048_576, base: :binary)
# => "1.0 MiB"

NumberF.format_duration(3725)            # => "1h 2m 5s"
NumberF.format_duration(3725, format: :clock)
# => "01:02:05"
NumberF.format_duration(90, format: :long)
# => "1 minute, 30 seconds"
```

### Significant figures

```elixir
NumberF.significant_figures(1234.5678, 3)  # => 1230.0
NumberF.significant_figures(0.00012345, 3) # => 0.000123
NumberF.format_significant(1.5, 4)         # => "1.500"
```

Significant figures count from the first non-zero
digit, so they are scale independent in a way that
decimal places are not.

## Calendar
{: .col-2}

### Quarters and fiscal years

```elixir
NumberF.quarter(~D[2024-08-22])       # => 3
NumberF.quarter_start(~D[2024-08-22]) # => ~D[2024-07-01]
NumberF.quarter_end(~D[2024-08-22])   # => ~D[2024-09-30]

# Named for the year the fiscal year ends in
NumberF.fiscal_year(~D[2024-08-22], 4) # => 2025
```

### Months and weeks

```elixir
NumberF.month_start(~D[2024-08-22]) # => ~D[2024-08-01]
NumberF.month_end(~D[2024-02-10])   # => ~D[2024-02-29]
NumberF.days_in_month(2024, 2)      # => 29
NumberF.leap_year?(1900)            # => false
NumberF.week_number(~D[2024-08-22]) # => 34
NumberF.age_in_months(~D[2020-01-15], ~D[2024-08-22])
# => 55
```

## Conversion & utilities
{: .col-2}

### Type coercion

```elixir
NumberF.to_int("42")        # => 42
NumberF.to_float("3.14")    # => 3.14
NumberF.to_decimal("1.10")
NumberF.to_boolean("true")  # => true
```

### Memory sizes

```elixir
NumberF.memory_size_cal(1_048_576)
# => "1.0 MB"

NumberF.memory_size_cal(5_000_000_000_000)
# => "4.55 TB"
```

### Phone numbers

```elixir
NumberF.format_phone("260977123456", "ZM")
NumberF.format_phone("14155552671", "US")
```

### Random strings

```elixir
NumberF.randomizer(8)
# => "RCKkn94U"  (random; yours will differ)

NumberF.default_password()
```

### Math

```elixir
NumberF.is_prime?(17)          # => true
NumberF.factorial(5)           # => 120
NumberF.gcd(12, 18)            # => 6
NumberF.lcm(4, 6)              # => 12
NumberF.combinations(5, 2)     # => 10
NumberF.to_radians(180)
NumberF.to_degrees(3.14159)
NumberF.interpolate(0, 100, 0, 10, 5)
```

### Introspection

```elixir
NumberF.modules()             # all NumberF modules
NumberF.modules_by_type(:formatting)
NumberF.function_categories() # list of category maps
NumberF.documentation()       # generated reference
```
