Language Reference

Formal specification for the CalcMark language.

Version: 1.0.0

This is the complete and authoritative specification for the CalcMark language.



Overview #

CalcMark is a calculation language that blends seamlessly with markdown. It allows calculations to live naturally within prose documents.

Design Goals #

  • Familiar: Syntax feels like calculator/spreadsheet usage
  • Minimal: Only essential features, no unnecessary complexity
  • Unambiguous: One way to do things, clear error messages
  • Unicode-aware: Full international character support
  • Markdown-compatible: Works within existing markdown documents

Key Characteristics #

  • Line-based: Each line is classified independently
  • Context-aware: Variables must be defined before use
  • Strongly typed: Minimal type coercion, clear type errors
  • Arbitrary precision: Decimal math for exact results

Philosophy #

Calculation by Exclusion #

CalcMark uses “calculation by exclusion” - if a line looks like markdown, it’s markdown. Only unambiguous calculations are treated as calculations.

# My Budget          -> MARKDOWN (header prefix)
salary = $5000       -> CALCULATION (assignment)
This is text         -> MARKDOWN (natural language)
5 + 3                -> CALCULATION (arithmetic)
- List item          -> MARKDOWN (bullet prefix)
-5 + 3               -> CALCULATION (negative number)
$100 budget          -> MARKDOWN (trailing text)

Explicit Over Implicit #

  • Spaces NOT allowed in identifiers (my_budget, not my budget)
  • Forward references not allowed (define before use)
  • Type mismatches are errors (no silent coercion)
  • Reserved keywords cannot be variable names

Document Model #

A CalcMark document is a sequence of lines. Each line is independently:

  1. Classified as BLANK, MARKDOWN, or CALCULATION
  2. Parsed (if CALCULATION)
  3. Validated (optional, produces diagnostics)
  4. Evaluated (if CALCULATION and valid)

Three Line Types #

TypeDescriptionExamples
BLANKEmpty or whitespace-only"", " ", "\t"
MARKDOWNProse, headers, lists, or invalid calculations"# Header", "Some text", "- Item"
CALCULATIONValid CalcMark expression or assignment"x = 5", "10 + 20", "salary * 12"

Frontmatter #

A CalcMark document can begin with a YAML frontmatter block delimited by ---. Frontmatter defines document-level configuration that is available to all calculations.

convert_to

Convert quantity results to a measurement system (si or imperial). Accepts a system name or a map with system and unit_categories. Valid categories: Area, Currency, Custom, DataSize, Energy, Length, Mass, Number, Power, Speed, Temperature, Volume.

Syntax
convert_to: <system>
Example
convert_to: si

exchange

Define currency conversion rates. Keys use FROM_TO format with 3-letter ISO 4217 codes (e.g., USD_EUR).

Syntax
exchange: FROM_TO: rate
Example
exchange: USD_EUR: 0.92

globals

Define document-wide variables. Values are CalcMark expressions evaluated before the document body.

Syntax
globals: name: value
Example
globals: tax_rate: 0.32 base_price: $100

scale

Multiply quantity results by a factor. Accepts a number or a map with factor and unit_categories. Currency scales only when Currency is listed in unit_categories. Valid categories: Area, Currency, Custom, DataSize, Energy, Length, Mass, Number, Power, Speed, Temperature, Volume. Temperature excluded by default.

Syntax
scale: <factor>
Example
scale: 2

Exchange Rates #

Define currency conversion rates using FROM_TO: rate format (underscore separator):

---
exchange:
  USD_EUR: 0.92
  EUR_GBP: 0.86
  USD_GBP: 0.79
  GBP_USD: 1.27
---

Rates are not automatically reversed. If you define USD_EUR, you must also define EUR_USD to convert in the other direction.

Global Variables #

Define values available throughout the document:

---
globals:
  tax_rate: 0.32
  base_price: $100
  start_date: Jan 15 2025
  bandwidth: 100 MB/s
---

Globals support all CalcMark literal types (numbers, currencies, quantities, dates, durations, rates, booleans, percentages). Expressions like 1 + 1 are not allowed – only literal values.

Scale #

Multiply all quantity results by a factor. Applied after evaluation, before display.

---
scale: 2
---

Scale accepts a number or a map with factor and optional unit_categories:

---
scale:
  factor: 4
  unit_categories: [Length, Mass]
---

Rules:

  • Scaling is explicit: you must specify unit_categories for any scaling to occur. A bare scale: 2 sets the factor but scales nothing.
  • Quantities in listed categories are multiplied by the factor
  • Currency scales only when Currency is listed in unit_categories
  • Number (unitless values) scales only when Number is listed in unit_categories
  • Boolean, Date, Duration, and Rate are always immune to scale
  • The special keyword All matches every category: unit_categories: [All]
  • Expressions containing @scale are exempt from scaling to prevent double-scaling

Valid categories: All, Area, Currency, Custom, DataSize, Energy, Length, Mass, Number, Power, Speed, Temperature, Volume.

@Directive References #

Use @scale and @globals.name to reference frontmatter values in expressions:

per_unit = total_cost / @scale
tax = income * @globals.tax_rate

@scale resolves to the numeric scale factor from frontmatter. Requires scale: to be defined.

@globals.name resolves to the typed value of a named global. Requires globals: with that key.

Validation rules:

ReferenceValid whenError otherwise
@scalescale: defined in frontmatter@scale requires 'scale:' in frontmatter
@globals.nameglobals: has name keyundefined global 'name'; defined globals: ...
@globals (no field)NeverParser error: @globals requires a field name
@globals.a.bNeverParser error: nested dots not supported
@exchange, @convert_to, @fooNevernot a supported directive; use @scale or @globals.name

@scale always resolves to a Number. @globals.name resolves to whatever type the global is (Number, Currency, Quantity, etc.).

Convert To #

Convert quantity results to a target measurement system. Applied after scale.

---
convert_to: si
---

Valid systems: si (metric) and imperial (US customary). Accepts a string or a map with system and optional unit_categories:

---
convert_to:
  system: imperial
  unit_categories: [Length, Volume]
---

Rules:

  • Quantities already in the target system are unchanged
  • Explicit in conversions override convert_to (the user chose the unit)
  • Custom units (e.g., eggs, servers) have no system mapping and are not converted
  • Currency, numbers, and other non-quantity types are unaffected
  • Rates have their amount converted, leaving the time denominator unchanged

Valid categories: All, Area, Currency, Custom, DataSize, Energy, Length, Mass, Number, Power, Speed, Temperature, Volume.

Transform Order #

When both scale and convert_to are present, transforms apply in this order:

  1. Evaluate all expressions
  2. Scale quantity results
  3. Convert to target measurement system

See the Recipe Scaling example for a complete walkthrough, and the User Guide — Frontmatter for a gentler introduction.

Template Interpolation #

Template variables embed calculated values into prose. After all calculations are evaluated, {{variable_name}} tags in text blocks are replaced with display-formatted results. Resolved values render bold in markdown and are wrapped in <span class="cm-interpolated"> in HTML.

Forward References #

The primary use case is forward references — a summary at the top of a document that pulls in values computed below:

## Executive Summary

| Metric | Value |
|--------|-------|
| Revenue | {{total_rev}} |
| Gross Margin | {{gross_margin}} |
| Team | {{team_hc}} |


total_rev = $4.2M
gross_margin = 28%
team_hc = 14 people

The summary table renders with $4.2M, 28%, and 14 people — even though the calculations appear below the text.

Inline Formatting #

Combine {{var}} with markdown formatting for emphasis, headings, and lists:

The grand total is {{total_cost}}.

_Team: {{headcount}}_

## Budget: {{annual_budget}}

- Revenue: {{revenue}}
- Costs: {{costs}}
- Margin: {{margin}}

Backticks around tags are consumed — `{{var}}` renders as value, not value. This prevents interpolated values from appearing as inline code.

Tables #

Interpolation works naturally in markdown tables. Use it for dashboard-style summaries:

| Scenario | Revenue | Margin |
|----------|---------|--------|
| Baseline | {{base_rev}} | {{base_margin}} |
| Optimistic | {{opt_rev}} | {{opt_margin}} |
| Stressed | {{stress_rev}} | {{stress_margin}} |

Syntax #

{{variable_name}} — variable names use word characters (letters, digits, underscore). Whitespace inside braces is tolerated: {{ total_rev }} resolves the same as {{total_rev}}.

Rules #

BehaviorDetail
Resolved valuesDisplay-formatted with locale, currency symbols, units, K/M/B suffixes
Markdown renderingResolved values are bold; backticks around tags are stripped
HTML renderingResolved values wrapped in <span class="cm-interpolated">
Scale / convert_toApplied before formatting — interpolated values match CalcBlock display
Missing variables{{unknown}} is left as-is in the output
ScopeText blocks only — {{var}} inside a CalcBlock is a syntax error, not interpolation
Round-trip safetySaving a .cm file preserves raw {{var}} tags (interpolation is render-time only)
Empty / expression tags{{}} and {{a + b}} are not matched
Directives{{@scale}} and {{@globals.name}} are not currently supported — use a variable alias

JSON output: The JSON formatter includes both source (raw text with {{var}} tags) and interpolated_source (resolved text) for programmatic consumers.

See the Services P&L example for a production use of interpolated summary tables, and the Household Budget for inline results in prose.

Line Classification #

Classification Rules #

Lines are classified in this order:

  1. BLANK — Empty or only whitespace
  2. INDENTED CODE → MARKDOWN — Line starts with 4+ spaces or a tab
  3. FENCED CODE BLOCK → MARKDOWN — Lines between ``` or ~~~ fences (stateful; all content inside is markdown regardless of what it looks like)
  4. MARKDOWN pattern — Matches a known CommonMark construct:
    • Block-level: # (ATX heading), > (blockquote), - / * / + (unordered list), digit. (ordered list), --- / *** / ___ (horizontal rule), === / --- (setext heading underline), ``` / ~~~ (fenced code fence)
    • Inline-level at start of line: ![ (image), [text](url) (inline link), [id]: url (link definition), **text** (bold formatting)
  5. CALCULATION — Attempt to parse and validate:
    • Starts with a literal (number, currency, boolean)
    • Contains assignment (=)
    • Is a valid expression
    • All variables are defined (context-aware)
  6. MARKDOWN (fallback) — Anything else

Context-Aware Classification #

x = 5               -> CALCULATION (assignment)
y = x + 10          -> CALCULATION (x is defined)
z = unknown * 2     -> MARKDOWN (unknown is undefined)

Edge Cases #

InputClassificationReason
$100 budgetMARKDOWNTrailing text after valid token
-5 + 3CALCULATIONNegative number (no space after -)
- 5MARKDOWNBullet list (space after -)
+ itemMARKDOWNUnordered list (+ marker with space)
x = 10MARKDOWNIndented code block (4-space prefix)
![alt](img.png)MARKDOWNImage syntax
[ref]: https://…MARKDOWNLink definition
x *MARKDOWNIncomplete expression
averageMARKDOWNNot reserved, not in context
avgMARKDOWNReserved keyword alone (not a valid expression)

Syntax & Grammar #

EBNF Grammar #

The grammar below covers the core expression hierarchy. Natural language function syntax (described in Natural Language Syntax) is parsed at the Primary level but omitted here for clarity.

Statement       ::= Assignment | Expression
Assignment      ::= IDENTIFIER "=" Expression
Expression      ::= Or
Or              ::= And ("or" And)*
And             ::= Comparison ("and" Comparison)*
Comparison      ::= Additive (ComparisonOp Additive)*
ComparisonOp    ::= ">" | "<" | ">=" | "<=" | "==" | "!="
Additive        ::= Multiplicative (("+"|"-") Multiplicative)*
                     ("as" ConversionTarget)?
ConversionTarget ::= "napkin" | "precise" | UnitName
Multiplicative  ::= Exponent (("*"|"/"|"%") Exponent)*
                     ("in" UnitName)?
                     ("per" TimeUnit)?
                     ("over" Expression)?
                     ("at" Expression "per" Expression ("with" Expression)?)?
Exponent        ::= Unary ("^" Unary)*
Unary           ::= ("not" | "-" | "+")* Postfix
Postfix         ::= Primary ("%" ("of" Expression)?)?
Primary         ::= Number | Currency | Quantity | Percentage
                   | Boolean | Date | Duration | Rate
                   | DirectiveRef | Identifier
                   | FunctionCall | "(" Expression ")"
FunctionCall    ::= FunctionName "(" (Expression ("," Expression)*)? ")"
DirectiveRef    ::= "@scale" | "@globals." IDENTIFIER

Operator Precedence #

From highest to lowest:

  1. Parentheses ()
  2. Exponentiation ^ (right-associative)
  3. Unary not, -, + (prefix)
  4. Postfix %, % of
  5. Multiplicative *, /, % (left-associative); postfix in, per, over, at
  6. Additive +, - (left-associative); postfix as
  7. Comparison >, <, >=, <=, ==, !=
  8. Logical AND and (left-associative)
  9. Logical OR or (left-associative)

Type System #

Data Types #

TypeExampleInternal
Number42, 3.14, 1,000Arbitrary-precision decimal
Percentage50%, 8.25%Fractional decimal (0.5, 0.0825)
Currency$100, €50.99Symbol + decimal
Booleantrue, false, yes, noBoolean
Quantity10 meters, 5 kg, 100 MBValue + unit
Duration5 days, 2 weeks, 1 yearValue + time unit
Rate100 MB/s, $50/hour, 1000 req/sNumerator / time unit
DateJan 15 2025, todayCalendar date

Type Compatibility #

Binary operations (preserve units):

Number + Number -> Number
Currency + Number -> Currency  (unit preserved)
Number + Currency -> Currency  (unit preserved)
Currency + Currency (same symbol) -> Currency
Currency + Currency (different symbols) -> Number  (units dropped)
Quantity + Quantity (same unit) -> Quantity
Date + Duration -> Date
Date - Date -> Duration
Rate * Duration -> Quantity  (via "over" keyword)
Number * Rate -> Rate        (scaling: 3 * 10 MB/s = 30 MB/s)
Rate * Number -> Rate        (commutative)
Rate * Quantity -> Quantity   (e.g., 10 MB/s * 500 MB = 5000 MB)
Quantity * Rate -> Quantity   (commutative)

Percentage widening:

When a percentage appears in addition or subtraction with another type, it widens to a proportion of that value:

$100 + 15% -> $115.00        (same as $100 * 1.15)
$100 - 15% -> $85.00         (same as $100 * 0.85)
10 kg + 50% -> 15 kg         (same as 10 kg * 1.5)
15% of 200 -> 30             ("of" syntax)

In multiplication or standalone use, percentages behave as their decimal value (50% = 0.5).

Functions (drop units when mixed):

avg($100, $200) -> $150.00  (same unit preserved)
avg($100, €200) -> 150  (Number, mixed units)
sqrt($100) -> $10.00  (single unit preserved)

Type errors:

Boolean + Number -> ERROR (no boolean arithmetic)
Quantity + Currency -> ERROR (incompatible types)

Literals #

Numbers #

42              Valid integer
3.14            Valid decimal
1,000           Thousands separator (comma)
1_000_000       Thousands separator (underscore)
0.5             Leading zero
.5              Invalid (must have leading zero)
1.2.3           Invalid (multiple decimal points)

Multiplier Suffixes #

10K             -> 10000
5M              -> 5000000
2B              -> 2000000000
1.5T            -> 1500000000000
1.5K            -> 1500

Scientific Notation #

1.2e10          -> 12000000000 (displayed as 12B)
5e3             -> 5000
2.5e-2          -> 0.025

Currency #

Currency literals use either a prefix symbol or a postfix ISO 4217 code.

Symbol syntax (prefix only):

$100            USD
$1,000.50       With separators
€50             EUR
£25.99          GBP
¥1000           JPY
100$            Invalid (symbol must be prefix)
$ 100           Invalid (no space between symbol and number)

Supported symbols: $ (USD), (EUR), £ (GBP), ¥ (JPY)

ISO code syntax (postfix):

Any 3-letter uppercase code works as a postfix currency identifier. Codes with a corresponding symbol display with that symbol; others display with the ISO code:

100 USD         -> $100.00
50 EUR          -> €50.00
25 GBP          -> £25.00
1000 JPY        -> ¥1,000
100 CHF         -> 100 CHF
50 CAD          -> 50 CAD

Semantic validation checks whether the code is a known ISO 4217 currency. Unknown codes produce an invalid_currency_code diagnostic. Currency conversion between different codes requires exchange: rates defined in Frontmatter.

Percentages #

Percentages are their own type, stored as a decimal fraction internally:

50%             -> Percentage (0.5 internally)
8.25%           -> Percentage (0.0825 internally)
15% of 200      -> 30  ("of" syntax)

See Type Compatibility for percentage widening rules.

Booleans #

Case-insensitive keywords:

true, false     Standard
yes, no         Natural language
t, f            Single letter
y, n            Single letter
True, FALSE     Any case

Quantities #

10 meters       Quantity: 10 in meters
5 kg            Quantity: 5 in kilograms
100 MB          Quantity: 100 in megabytes

Custom Units #

Any identifier following a number becomes a unit. CalcMark does not require units to be predefined — these are called custom units:

5 apples        Quantity: 5 apples
1000 req/s      Rate: 1000 requests per second
10 servers      Quantity: 10 servers

Arithmetic with matching custom units preserves the unit. Mismatched custom units produce an error:

5 apples + 3 apples    -> 8 apples
10 servers * 2         -> 20 servers
5 apples + 3 oranges   -> ERROR (incompatible units)

Rates #

100 MB/s        Rate: 100 megabytes per second
$50/hour        Rate: $50 per hour
1000 req/s      Rate: 1000 requests per second
$120000/year    Rate: $120,000 per year

Dates #

Jan 15 2025     Date literal
Dec 25 2025     Date literal
today           Current date
tomorrow        Tomorrow's date
yesterday       Yesterday's date

Durations #

5 days          Duration
2 weeks         Duration
3 months        Duration
1 year          Duration

Identifiers #

  • Must start with letter, underscore, or Unicode character (not digit)
  • Can contain letters, digits, underscores, Unicode, emoji
  • Cannot be reserved keywords or constants
  • Spaces NOT allowed (use underscores)
x               Valid
salary          Valid
tax_rate        Valid (use underscores, not spaces)
_private        Valid (underscore prefix)
123abc          Invalid (cannot start with digit)
my budget       Invalid (spaces not allowed, use my_budget)
avg             Invalid (reserved keyword)
PI              Invalid (reserved constant)

Mathematical Constants #

Built-in constants (read-only, case-insensitive):

ConstantValue
PI, pi3.141592653589793
E, e2.718281828459045

Constants cannot be assigned:

PI = 3          ERROR: Cannot assign to constant 'PI'
radius = 5
area = PI * radius ^ 2
Results
radius = 55
area = PI * radius ^ 278.539816

Operators #

Arithmetic #

OperatorNameExampleResultAssociativity
^Exponent2 ^ 38Right
*Multiply3 * 412Left
/Divide10 / 25Left
%Modulus10 % 31Left
+Add5 + 38Left
-Subtract5 - 32Left

Comparison #

OperatorNameExampleResult
>Greater than5 > 3true
<Less than5 < 3false
>=Greater or equal5 >= 5true
<=Less or equal5 <= 3false
==Equal5 == 5true
!=Not equal5 != 3true

Logical #

OperatorNameExampleResult
andLogical ANDtrue and falsefalse
orLogical ORtrue or falsetrue
notLogical NOTnot truefalse

Case-insensitive: AND, and, And all work.

Unary #

OperatorNameExampleResult
-Negation-5-5
+Plus+55
notLogical NOTnot truefalse

Conversion and Postfix #

OperatorNameExampleResult
inUnit conversion10 meters in feet32.81 feet
asDisplay modifier1234567 as napkin~1.2M
perRate denominator100 MB per day100 MB/day
overAccumulation100 MB/s over 1 day~8.64 TB
at...perCapacity10 TB at 2 TB per disk5 disk
% ofPercentage of15% of 20030
fromDate offset2 days from today(date)

Assignment #

OperatorNameExampleEffect
=Assignx = 5Stores 5 in variable x

Reserved Keywords #

These words cannot be used as variable names.

Logical Operators #

and, or, not

Case-insensitive: AND, and, And all work.

Control Flow (Reserved for Future) #

if, then, else, elif, end
for, while
return, break, continue
let, const

Function Names #

All built-in function names are reserved:

avg, sum, sqrt, number, accumulate, convert_rate, capacity,
downtime, rtt, throughput, transfer_time,
read, seek, compress, compound, grow, depreciate

Language Keywords #

in, as, of, per, over, at, from, with, napkin, precise

Contextual Keywords #

These words have special meaning in specific syntactic positions but are not reserved as variable names:

by, compounded, to, monthly, quarterly, weekly, daily, yearly
compound $1000 by 5% monthly over 10 years         (by, monthly)
compound $1000 by 5% compounded monthly over 10    (by, compounded)
depreciate $50000 by 15% over 5 to $5000           (to)

Bare frequency adverbs (monthly, quarterly, weekly, daily, yearly) are shorthand for compounded monthly, compounded quarterly, etc.


Functions #

accumulate

Calculate total from a rate over time

Syntax
accumulate(rate, time)
Example
accumulate(100 req/s, 1 hour) → 360000 req

avg

Calculate the average of values

Syntax
avg(a, b, c, ...)
Aliases
average, mean, average of (input syntax)
Example
avg(10, 20, 30) → 20

capacity

Calculate how many units needed for a given load

Syntax
capacity(demand, capacity_per_unit, unit) or capacity(demand, capacity_per_unit, unit, buffer)
Aliases
requires
Example
capacity(10000 req/s, 500 req/s, server) → 20 servers

compound

Calculate compound growth over time periods

Syntax
compound(principal, rate, periods, modifier?)
Aliases
compound...by...over (input syntax), compound...by...monthly...over (input syntax)
Example
compound(1000, 5%, 10 years, monthly) → 1647.01

compress

Estimate compressed data size

Syntax
compress(size, algorithm)
Aliases
compress...using (input syntax)
Example
compress 1 GB using gzip

convert_rate

Convert a rate to a different time unit

Syntax
convert_rate(rate, unit)
Example
convert_rate(1000 req/s, minute) → 60000 req/min

depreciate

Calculate declining balance depreciation over time

Syntax
depreciate(value, rate, periods, salvage?)
Aliases
depreciate...by...over...to (input syntax)
Example
depreciate(10000, 20%, 5) → 3276.80

downtime

Calculate downtime from availability percentage

Syntax
downtime(availability, period)
Example
downtime(99.9%, month) → 43.2 minutes

grow

Calculate linear growth by adding a fixed amount each period

Syntax
grow(amount, increment, periods)
Aliases
grow...by...over (input syntax)
Example
grow(100, 20 GB, 5) → 200 GB

number

Extract the numeric value from any type

Syntax
number(value)
Example
number(10 kg) → 10

read

Time to read data from storage

Syntax
read(size, storage_type)
Aliases
read...from (input syntax)
Example
read 100 MB from ssd

rtt

Network round-trip time for a scope

Syntax
rtt(scope)
Aliases
round trip time
Example
rtt(regional) → 10 ms

seek

Access latency for storage type

Syntax
seek(storage_type)
Example
seek(hdd) → 10 ms

sqrt

Calculate the square root

Syntax
sqrt(n)
Aliases
square root of (input syntax)
Example
sqrt(16) → 4

sum

Calculate the sum of values

Syntax
sum(a, b, c, ...)
Aliases
sum of (input syntax), total
Example
sum($100, $200, $300) → $600

throughput

Network bandwidth for a connection type

Syntax
throughput(network_type)
Example
throughput(gigabit) → 125 MB/s

transfer_time

Time to transfer data over a network

Syntax
transfer_time(size, scope, network)
Aliases
transfer...across (input syntax)
Example
transfer 1 GB across regional gigabit

For detailed examples of every function, including natural language syntax forms, see the User Guide: Function Reference.

Unit Handling in Functions #

Same units are preserved:

avg($100, $200, $300) -> $200.00
sqrt($100) -> $10.00

Mixed units are dropped:

avg($100, €200) -> 150  (no units)
average of $50, €100, £150 -> 100  (no units)

Natural Language Syntax #

CalcMark supports natural language forms for many functions. These are equivalent to the function-call syntax. Arguments can be literal values (100 MB) or variable references (data).

PatternEquivalent
average of X, Y, Zavg(X, Y, Z)
sum of X, Y, Zsum(X, Y, Z)
square root of Xsqrt(X)
X over Yaccumulate(X, Y)
X at Y per Zcapacity(X, Y, Z)
X at Y per Z with W% buffercapacity(X, Y, Z, W%)
read X from Yread(X, Y)
compress X using Ycompress(X, Y)
transfer X across Y Ztransfer_time(X, Y, Z)
compound X by Y% over Zcompound(X, Y%, Z)
compound X by Y% monthly over Zcompound(X, Y%, Z, monthly)
compound X by Y% per P over Zcompound(X, Y%, Z, P)
compound X by Y% compounded F over Zcompound(X, Y%, Z, compounded F)
grow X by Y over Zgrow(X, Y, Z)
depreciate X by Y% over Zdepreciate(X, Y%, Z)
depreciate X by Y% over Z to Wdepreciate(X, Y%, Z, W)

See the User Guide: Natural Language Syntax for the complete reference table with examples.


Napkin Math #

The as napkin modifier rounds results to 2 significant figures and normalizes units. See the User Guide: Napkin Math for usage examples.

Syntax: expression as napkin

Works with: Number, Quantity, Currency, Duration, Rate


Precise Display #

The as precise modifier shows full float precision, skipping all display rounding. This is the opposite of as napkin and is useful when you need exact values from unit conversions.

Syntax: expression as precise

Can be chained after a unit conversion: 10 meters as feet as precise

Works with: Number, Quantity, Currency, Duration, Rate


Rates #

Rates are defined using slash syntax (e.g., 100 MB/s, $50/hour). See the User Guide: Rates for rate accumulation with over and rate conversion.

Rate Arithmetic Widening #

When a rate appears on the right side of * or /, its time denominator is dropped and the rate’s amount is used instead. This is called widening — the rate widens into its underlying quantity.

When a rate appears on the left side, it stays a rate. This lets you scale rates naturally.

Operand order determines the result type:

ExpressionLeftRightResultWhy
rate * 3RateNumberRateRate on left → stays rate (scaling)
3 * rateNumberRateQuantityRate on right → widened
rate / 2RateNumberRateRate on left → stays rate
100 / rateNumberRateNumberRate on right → widened
rate * qtyRateQuantityQuantityCross-type, extracts amount
qty * rateQuantityRateQuantityRate on right → widened
rate / rateRateRateNumberSame-unit ratio (no widening)
posts_rate = 2 posts/week
scaled = posts_rate * 3           -> 6 posts/week  (Rate — rate on left)
total  = 3 * posts_rate           -> 6 posts       (Quantity — rate on right)
half   = posts_rate / 2           -> 1 posts/week  (Rate — rate on left)

This rule is asymmetric by design. The operand on the left is the “subject” of the expression:

  • read_rate * peak_multiplier — you are scaling a rate, so the result is a rate.
  • daily_users * posts_per_user_per_week — you are scaling a count by a rate, so the result is a quantity.

Rate widening only applies to binary * and /. It does not affect functions like accumulate(), over, or per.


Date Arithmetic #

CalcMark supports date literals (Jan 15 2025, today), duration arithmetic, and the from keyword. See the User Guide: Date Arithmetic for details.


Network Functions #

CalcMark provides rtt, throughput, transfer_time, and downtime for network planning. See the User Guide: Network Functions for scope tables and examples.


Storage Functions #

CalcMark provides read, seek, and compress for storage planning. See the User Guide: Storage Functions for device type tables and examples.


Growth Functions #

CalcMark provides compound, grow, and depreciate for modeling growth and depreciation over time.

Compound Growth #

Simple compound growthA = P(1+r)^t, compounding once per year. The 3rd argument is years:

compound($1000, 5%, 10)                              -> $1628.89
compound(500 customers, 20%, 12)                     -> 4458.05 customers

Financial compoundingA = P(1+r/n)^(nt), compounding multiple times per year. Triggered by a frequency adverb (monthly, quarterly, weekly, daily, yearly):

compound($1000, 5%, 10, monthly)                     -> $1647.01
compound($1000, 5%, 10, quarterly)                   -> $1643.62
compound($1000, 5%, 24 months, monthly)              -> $1103.43

Natural language forms:

compound $1000 by 5% over 10 years
compound $1000 by 5% monthly over 10 years
compound $1000 by 5% per month over 12 months
Results
compound $1000 by 5% over 10 years$1,628.89
compound $1000 by 5% monthly over 10 years$1,647.01
compound $1000 by 5% per month over 12 months$1,795.86

The frequency adverb controls how many times per year the rate is applied. Without it, the rate compounds once per year. With monthly, the annual rate is split into 12 applications per year.

Linear Growth #

grow($500, $100, 36)               -> $4100.00
grow 100 by 20 over 5 months       -> 200  (NL form)

Depreciation #

depreciate($50000, 15%, 5)                -> $22185.27
depreciate($50000, 15%, 20, $5000)        -> $5000.00  (salvage floor)
depreciate $50000 by 15% over 5 years     -> (NL form)
depreciate $50000 by 15% over 5 years to $5000
Results
depreciate $50000 by 15% over 5 years to $5000$22.19K

See the User Guide: Growth Functions for the full argument reference.


Validation & Diagnostics #

Diagnostic Levels #

SeverityMeaning
ErrorPrevents evaluation, line becomes MARKDOWN
WarningLine evaluates but issue is noted
HintSuggestion or style recommendation

Diagnostic Codes #

CodeSeverityDescription
type_mismatchErrorIncompatible types in operation
division_by_zeroErrorDivision or modulus by zero
invalid_currency_codeErrorUnsupported currency symbol
incompatible_currenciesErrorMixed currency codes without exchange rate
incompatible_unitsErrorUnit mismatch in operation
unsupported_unitErrorUnit not recognized for conversion
invalid_dateErrorInvalid date literal
invalid_monthErrorMonth name or number out of range
invalid_dayErrorDay out of range for the given month
invalid_yearErrorInvalid year value
invalid_leap_yearErrorFeb 29 in a non-leap year
invalid_date_operationErrorInvalid operation on date types
invalid_directiveErrorInvalid @ directive reference
undefined_globalError@globals.name references undefined global
missing_frontmatterWarningDirective requires frontmatter section
undefined_variableWarningVariable used before definition
variable_redefinitionWarningVariable assigned more than once
mixed_base_unitsHintMixing binary (KiB) and decimal (KB) data units