Skip to main content

Domain Model: Maya Calendar System

This document explains the Maya calendar system as implemented in this library, including the mathematical relationships and historical context.

Overview

The ancient Maya civilization used multiple interlocking calendar systems to track time. This library implements the three main components:

  1. Tzolkin - Sacred 260-day cycle
  2. Haab - Solar 365-day approximation
  3. Long Count - Absolute chronology (linear)

The Tzolkin (Sacred Calendar)

Structure

The Tzolkin is formed by combining two cycles:

  • 13 numbers (coefficients): 1, 2, 3, ..., 13
  • 20 day names: Imix, Ik', Ak'bal, K'an, ...
Day 1:  1 Imix
Day 2: 2 Ik'
Day 3: 3 Ak'bal
...
Day 20: 7 Ajaw
Day 21: 8 Imix (coefficient wraps at 13, day wraps at 20)
...
Day 260: 13 Ajaw (both cycles complete)
Day 261: 1 Imix (starts over)

The 20 Day Names

#NameMeaning#NameMeaning
1ImixEarth/Crocodile11ChuwenMonkey
2Ik'Wind12EbRoad
3Ak'balNight13BenReed
4K'anCorn14IxJaguar
5ChikchanSerpent15MenEagle
6KimiDeath16KibWax
7Manik'Deer17KabanEarth
8LamatVenus18Etz'nabFlint
9MulukWater19KawakStorm
10OkDog20AjawLord

Mathematics

Since GCD(13, 20) = 1, the cycle length is:

LCM(13, 20) = 13 × 20 = 260 days

Implementation

class Tzolkin {
coeff: ICoefficient; // 1-13 or wildcard
day: TzolkinDay | Wildcard; // 20 names or wildcard

next(): Tzolkin {
const newCoeff = this.coeff.next(); // Cycles 1→13→1
const newDay = this.day.next(); // Cycles through 20 names
return getTzolkin(newCoeff, newDay);
}
}

The Haab (Solar Year)

Structure

The Haab approximates the solar year with:

  • 18 months of 20 days each = 360 days
  • 1 short month (Wayeb) of 5 days = 5 days
  • Total: 365 days (close to solar year of ~365.25 days)

The 19 Month Names

#NameDaysSeason
1Pop0-19Spring
2Wo0-19
3Sip0-19
4Sotz'0-19
5Sek0-19Summer
6Xul0-19
7Yaxk'in0-19
8Mol0-19
9Ch'en0-19Fall
10Yax0-19
11Sak0-19
12Keh0-19
13Mak0-19Winter
14K'ank'in0-19
15Muwan0-19
16Pax0-19
17K'ayab0-19
18Kumk'u0-19
19Wayeb0-4Unlucky days

Day Numbering

Unlike modern calendars, Haab days are numbered 0-19 (not 1-20):

0 Pop  (first day of Pop)
1 Pop
2 Pop
...
19 Pop (last day of Pop)
0 Wo (first day of Wo)

Implementation

class Haab {
coeff: ICoefficient; // 0-19 or wildcard
month: HaabMonth | Wildcard; // 19 names or wildcard

next(): Haab {
let newCoeff = this.coeff.next();
let newMonth = this.month;

// Special handling for month transitions
if (this.coeff.toString() === '19' && this.month.toString() !== 'Wayeb') {
newCoeff = new NumberCoefficient(0);
newMonth = this.month.next();
} else if (this.coeff.toString() === '4' && this.month.toString() === 'Wayeb') {
newCoeff = new NumberCoefficient(0);
newMonth = /* back to Pop */;
}

return getHaab(newCoeff, newMonth);
}
}

The Calendar Round (Tzolkin + Haab)

The 52-Year Cycle

When Tzolkin (260 days) and Haab (365 days) run concurrently:

LCM(260, 365) = 18,980 days ≈ 52 solar years

This means the same Tzolkin-Haab combination only recurs every 52 years.

Example Progression

Day 1:   1 Imix 0 Pop
Day 2: 2 Ik' 1 Pop
Day 3: 3 Ak'bal 2 Pop
...
Day 365: 2 Ik' 0 Pop
Day 366: 3 Ak'bal 1 Pop
...
Day 18,980: 1 Imix 0 Pop (cycle complete!)

Historical Significance

A person would only see the same Calendar Round date repeat once in their lifetime (if they lived past 52). For dates beyond a human lifespan, the Long Count is needed.

Implementation

class CalendarRound {
tzolkin: Tzolkin;
haab: Haab;

next(): CalendarRound {
// Both cycles advance independently
return getCalendarRound(
this.tzolkin.next(),
this.haab.next()
);
}

diff(other: CalendarRound): DistanceNumber {
// Find distance within 18,980-day cycle
// Complex algorithm handles wrap-around
}
}

The Long Count (Absolute Chronology)

Purpose

The Long Count provides absolute chronology - a day count from a mythological creation date.

Base-20 Vigesimal System

PositionNameMultiplierExample
4B'ak'tun144,000 days9
3K'atun7,200 days17
2Tun360 days0
1Winal20 days0
0K'in1 day0

Example: 9.17.0.0.0

= 9 × 144,000 days
+ 17 × 7,200 days
+ 0 × 360 days
+ 0 × 20 days
+ 0 × 1 days
= 1,296,000 + 122,400
= 1,418,400 days since creation

The Creation Date

Maya Mythology: 0.0.0.0.0 = 4 Ajaw 8 Kumk'u

Correlation to Western Calendar:

  • GMT (584283): August 11, 3114 BCE (Proleptic Gregorian)
  • This is the most widely accepted correlation

Mixed-Radix Exception

⚠️ Important: The Winal position uses base-18, not base-20!

Reason: 18 Winals × 20 days = 360 days ≈ solar year

This makes the Tun approximately equal to a solar year.

Implementation

class LongCount extends DistanceNumber {
parts: (number | Wildcard)[];

getTotalDays(): number {
const multipliers = [1, 20, 360, 7200, 144000, ...];
return this.parts.reduce((sum, part, i) => {
return sum + (typeof part === 'number' ? part * multipliers[i] : 0);
}, 0);
}

getJulianDay(): number {
return this.correlationConstant.jdnOrigin + this.getTotalDays();
}
}

The Full Date (Complete Specification)

Why Both?

SystemPrecisionUniqueness
Calendar RoundWithin 52 yearsAmbiguous for events > 52 years apart
Long CountAbsoluteUnique across millennia
Full DateAbsolute + CyclicBest of both worlds

Example

4 Ajaw 8 Kumk'u 9.17.0.0.0
└─────┬──────┘ └─────┬─────┘
Calendar Round Long Count
(repeats every (absolute
52 years) chronology)

Verification

The Calendar Round components must match what the Long Count implies:

  • 9.17.0.0.0 → 1,418,400 days since creation
  • 1,418,400 mod 260 = specific Tzolkin date
  • 1,418,400 mod 365 = specific Haab date
  • Must equal "4 Ajaw 8 Kumk'u"

Implementation

class FullDate {
cr: CalendarRound;
lc: LongCount;

equal(other: IPart): boolean {
if (other instanceof FullDate) {
// Both components must match
return this.cr.equal(other.cr) && this.lc.equal(other.lc);
}
return false;
}

isPartial(): boolean {
// Partial if either component has wildcards
return this.cr.isPartial() || this.lc.isPartial();
}
}

Additional Calendar Elements

Lord of the Night (G-Glyphs)

A 9-day cycle associated with Long Count dates:

G1, G2, G3, G4, G5, G6, G7, G8, G9, (repeat)

Calculation:

getLordOfTheNight(): LordOfTheNight {
const position = (this.getTotalDays() + 1) % 9;
return lords[position];
}

Example: 9.17.0.0.0 → G9

Correlation Constants

Maps Maya dates to Western calendars:

class CorrelationConstant {
readonly correlationConstant: number; // 584283 for GMT
readonly jdnOrigin: number; // Julian Day Number of 0.0.0.0.0

constructor(constant: number) {
this.correlationConstant = constant;
this.jdnOrigin = constant;
}
}

Usage: LongCount + Correlation Constant → Julian Day Number → Gregorian Date


Wildcards in the Domain Model

Purpose

Wildcards represent unknown or variable portions of dates:

  • Historical gaps: "* Ajaw 8 Kumk'u 9.17.0.0.0" (coefficient unknown)
  • Pattern matching: "4 Ajaw * * 9.17.0.0.0" (Haab unknown)
  • Date queries: "* * * * 9.*.*.*.0" (find all matching)

Implementation

class Wildcard extends CommentWrapper implements IPart {
toString(): string {
return '*';
}

equal(other: IPart): boolean {
return isWildcard(other); // Wildcards equal each other
}
}

Wildcard Positions

Any component can be a wildcard:

// Calendar Round wildcards
type CoefficientType = NumberCoefficient | WildcardCoefficient;
type DayType = TzolkinDay | Wildcard;
type MonthType = HaabMonth | Wildcard;

// Long Count wildcards
type LongCountPart = number | Wildcard;

Expansion

const partial = new FullDateFactory().parse('* Ajaw 8 Kumk\'u 9.17.0.0.0');
const expander = new FullDateWildcard(partial);
const allMatches = expander.run();

// Returns all dates with format: [1-13] Ajaw 8 Kumk'u 9.17.0.0.0
console.log(allMatches.length); // 13 (one for each coefficient)

Mathematical Relationships

Cycle Periods

CyclePeriodFormula
Tzolkin260 days13 × 20
Haab365 days18 × 20 + 5
Calendar Round18,980 daysLCM(260, 365)
Tun360 days18 × 20
K'atun7,200 days20 × 360
B'ak'tun144,000 days20 × 7,200

Why 52 Years?

Calendar Round = 18,980 days
Solar Year ≈ 365.25 days
18,980 ÷ 365.25 ≈ 52 years

Long Count Day Calculation

function longCountToDays(parts: number[]): number {
const [kin, winal, tun, katun, baktun, ...higher] = parts;

let total = kin; // 1
total += winal * 20; // 20
total += tun * 20 * 18; // 360
total += katun * 20 * 18 * 20; // 7,200
total += baktun * 20 * 18 * 20 * 20; // 144,000

// Higher cycles (rare)
let multiplier = 144,000;
for (const cycle of higher) {
multiplier *= 20;
total += cycle * multiplier;
}

return total;
}

Western Calendar Conversion

Julian Day Number (JDN)

An intermediate representation for calendar conversions:

  • Definition: Days since January 1, 4713 BCE (Proleptic Julian)
  • Purpose: Universal day count independent of calendar system

Conversion Chain

Long Count → Julian Day Number → Gregorian/Julian Date
↓ ↓ ↓
1,418,400 + 584283 → Dec 21, 2012
(days since (correlation (Gregorian)
creation) constant)

Implementation

class LongCount {
getJulianDay(): number {
return this.correlationConstant.jdnOrigin + this.getTotalDays();
}

asGregorian(): GregorianCalendarDate {
return new GregorianCalendarDate(this.getJulianDay());
}

asJulian(): JulianCalendarDate {
return new JulianCalendarDate(this.getJulianDay());
}
}

Correlation Debate

Multiple theories exist for Maya-Gregorian alignment:

ConstantName0.0.0.0.0 DateStatus
584283GMTAug 11, 3114 BCEMost accepted
584285Modified GMTAug 13, 3114 BCEAlternative
584281SpindenOct 14, 3114 BCELess common

This library defaults to GMT (584283) but allows configuration.


Domain Invariants

1. Calendar Round Synchronization

Invariant: For any Full Date, the CR must match what the LC implies.

// Given LC, calculate expected CR
const lc = new LongCount(9, 17, 0, 0, 0);
const days = lc.getTotalDays(); // 1,418,400

const expectedTzolkin = origin.shift(days % 260);
const expectedHaab = originHaab.shift(days % 365);
const expectedCR = getCalendarRound(expectedTzolkin, expectedHaab);

// Full date must match
const fullDate = new FullDate(expectedCR, lc); // Valid
const badDate = new FullDate(wrongCR, lc); // Inconsistent (not enforced)

Note: The library currently doesn't enforce this invariant; it's the user's responsibility.

2. Winal Range Restriction

Invariant: Winal position (position 1 in LC) must be 0-17, not 0-19.

// Valid
new LongCount(9, 17, 0, 0, 0); // ✓ Winal = 0

// Invalid (would violate Tun definition)
new LongCount(9, 19, 0, 0, 0); // ✗ Winal = 19 (not allowed)

Why: Because Tun = 18 Winals = 360 days (not 20 × 20 = 400)

3. Haab Coefficient Ranges

Invariant: Coefficients depend on month:

  • Regular months: 0-19 (20 days)
  • Wayeb: 0-4 (5 days only)
// Valid
new Haab(new NumberCoefficient(4), getHaabMonth('Wayeb')); // ✓

// Invalid
new Haab(new NumberCoefficient(19), getHaabMonth('Wayeb')); // ✗ Wayeb only has 5 days

Comments as Metadata

Purpose

Attach scholarly annotations, historical context, or notes to dates:

const date = factory.parse('4 Ajaw 8 Kumk\'u 9.17.0.0.0');

date
.setComment('Creation date')
.appendComment('According to the Popol Vuh');

// Comments preserved through operations
const next = date.shift(1);
console.log(next.comment.text); // Inherited

Use Cases

  • Historical context
  • Scholarly notes
  • Source citations
  • Uncertainty indicators
  • Translation notes

Distance Numbers

Purpose

Represent relative time spans rather than absolute dates.

Difference from Long Count

Long CountDistance Number
Absolute dateRelative duration
Always positiveCan be negative
Has correlation constantNo correlation
Converts to western datesRepresents interval

Operations

const interval = new DistanceNumber(0, 0, 1, 0, 0);  // 1 Tun = 360 days

// Sign handling
console.log(interval.isPositive); // true
console.log(interval.sign); // 1

const negative = new DistanceNumber(0, 0, 0, 0, -100);
console.log(negative.isNegative); // true
console.log(negative.sign); // -1

Pattern Matching with Wildcards

Single Wildcard

const pattern = '* Ajaw 8 Kumk\'u 9.17.0.0.0';
const expander = new FullDateWildcard(factory.parse(pattern));
const matches = expander.run();

// Returns 13 dates: 1 Ajaw ..., 2 Ajaw ..., ..., 13 Ajaw ...

Multiple Wildcards

const pattern = '* * * Kumk\'u 9.17.0.0.0';
// ↑ ↑ ↑
// coeff, day, haab-coeff all unknown

const matches = expander.run();
// Returns 13 × 20 × 20 = 5,200 matching dates

Cartesian Product

For n wildcards with ranges r₁, r₂, ..., rₙ:

Total matches = r₁ × r₂ × ... × rₙ

Example: * * * * *.*.*.*.*

Note: This is an approximation to demonstrate combinatorial explosion. The exact count varies because:

  • Haab coefficient range depends on the month (0-19 for regular months, 0-4 for Wayeb)
  • Higher Long Count cycles beyond B'ak'tun are theoretically unbounded

Approximate calculation:

≈ 13 × 20 × (avg ~18) × 19 × (varies) × 20 × 18 × 18 × 20
≈ billions of combinations

Validation Rules

Tzolkin

  • Coefficient: 1-13 (no 0)
  • Day: Must be one of 20 valid names
  • Both can be wildcards

Haab

  • Coefficient: 0-19 for regular months, 0-4 for Wayeb
  • Month: Must be one of 19 valid names
  • Both can be wildcards

Long Count

  • Parts: Each position 0-19, except Winal (0-17)
  • Can extend to higher cycles (Pictun, Calabtun, etc.)
  • Any position can be wildcard
  • Must have at least 2 parts (K'in and Winal minimum)

Historical Context

Timeline of Maya Calendar

  • Classic Period: 250-900 CE (Long Count widely used)
  • Post-Classic: 900-1521 CE (Calendar Round more common)
  • Spanish Conquest: 1521 CE (calendar use declined)
  • Modern Revival: 20th-21st century (scholarly reconstruction)

The 2012 Phenomenon

Date: 13.0.0.0.0 (December 21, 2012)

Significance:

  • End of 13th B'ak'tun
  • Beginning of 14th B'ak'tun (0.0.0.0.1)
  • Not "end of calendar" (popularly misunderstood)
  • Just like odometer rolling over

Calendar continues: 14.0.0.0.0, 15.0.0.0.0, etc.


Domain Model Diagram

┌─────────────────────────────────────────────────────────────┐
│ FullDate │
│ "4 Ajaw 8 Kumk'u 9.17.0.0.0" │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────────┐ ┌──────────────────────────┐ │
│ │ CalendarRound │ │ LongCount │ │
│ │ "4 Ajaw 8 Kumk'u" │ │ "9.17.0.0.0" │ │
│ ├────────────────────────┤ ├──────────────────────────┤ │
│ │ │ │ extends DistanceNumber │ │
│ │ ┌──────────────────┐ │ │ parts: [0,0,0,17,9] │ │
│ │ │ Tzolkin │ │ │ + arithmetic │ │
│ │ │ "4 Ajaw" │ │ │ + western conversion │ │
│ │ ├──────────────────┤ │ │ + lord of night │ │
│ │ │ coeff: 4 │ │ │ + correlation constant │ │
│ │ │ day: Ajaw │ │ └──────────────────────────┘ │
│ │ └──────────────────┘ │ │
│ │ │ │
│ │ ┌──────────────────┐ │ │
│ │ │ Haab │ │ │
│ │ │ "8 Kumk'u" │ │ │
│ │ ├──────────────────┤ │ │
│ │ │ coeff: 8 │ │ │
│ │ │ month: Kumk'u │ │ │
│ │ └──────────────────┘ │ │
│ └────────────────────────┘ │
│ │
│ All extend CommentWrapper (comment metadata) │
│ All implement IPart (equality comparison) │
└─────────────────────────────────────────────────────────────┘

Type Hierarchy

Class Hierarchy

Object

├── Comment
│ └── (standalone, not inherited)

├── CommentWrapper (abstract mixin)
│ ├── Wildcard
│ ├── DistanceNumber (abstract)
│ │ └── LongCount
│ ├── CalendarRound
│ ├── Tzolkin
│ ├── Haab
│ ├── FullDate
│ ├── LongcountOperation (abstract)
│ │ ├── LongcountAddition
│ │ └── LongcountSubtraction
│ └── *Wildcard (operation classes)

├── Base (abstract)
│ └── Cycle (abstract)
│ ├── HaabMonth
│ └── TzolkinDay

├── ICoefficient (interface)
│ ├── NumberCoefficient
│ └── WildcardCoefficient

└── Factory (abstract)
├── CalendarRoundFactory
├── LongCountFactory
├── FullDateFactory
└── GregorianFactory

Interface Implementation

IPart (interface)
├── Wildcard ✓
├── Comment ✗ (doesn't implement)
├── DistanceNumber ✓
├── LongCount ✓
├── CalendarRound ✓
├── Tzolkin ✓
├── Haab ✓
├── FullDate ✓
└── All Operations ✓

Calendar Mathematics

Modular Arithmetic

Calendar cycling relies heavily on modular arithmetic:

// Tzolkin coefficient: cycles 1-13
nextCoeff = ((current - 1 + 1) % 13) + 1;
// Subtract 1 (0-indexed), add increment, mod 13, add 1 back

// Tzolkin day: cycles 0-19
nextDay = (current + 1) % 20;

// Haab coefficient: cycles 0-19 (or 0-4 for Wayeb)
nextCoeff = (current + 1) % (maxForMonth + 1);

Long Count Carries

// Adding 1.0.0.0.19 + 0.0.0.0.1
// B K T W K W K
Input: 1. 0. 0. 0.19 + 0. 0. 0. 0. 1

K'in: 19 + 1 = 20, write 0, carry 1
Winal: 0 + 0 + 1 = 1, write 1, carry 0
Result: 1. 0. 0. 1. 0

Mixed-Radix Complexity

// Position-dependent modulo
function getModulo(position: number): number {
if (position === 1) {
return 18; // Winal → Tun transition
}
return 20; // All other positions
}

// Example: 0.0.0.17.19 + 0.0.0.0.1
// T W K
Input: 0. 0.17.19 + 0. 0. 0. 1
K'in: 19 + 1 = 200, carry 1
Winal: 17 + 0 + 1 = 180, carry 1 (max 17, not 19!)
Tun: 0 + 0 + 1 = 1
Result: 0. 1. 0. 0

Edge Cases

Wayeb (5-Day Month)

// Last day of Wayeb
const lastWayeb = getHaab(
new NumberCoefficient(4),
getHaabMonth('Wayeb')
);

const nextDay = lastWayeb.next();
console.log(nextDay.toString()); // "0 Pop" (wraps to new year)

Long Count Overflows

// 19.19.19.17.19 + 0.0.0.0.1
const lc = new LongCount(19, 19, 19, 17, 19);
const result = lc.plus(new DistanceNumber(0, 0, 0, 0, 1)).run();
console.log(result.toString()); // "20.0.0.0.0" (adds new position)

Zero Dates

// 0.0.0.0.0 is valid (creation date)
const creation = new LongCount(0, 0, 0, 0, 0);
console.log(creation.asGregorian()); // Aug 11, 3114 BCE (GMT)

// Calendar Round of creation
const cr = origin; // 4 Ajaw 8 Kumk'u

Gregorian-Julian Calendar Transition

The 1582 Gap

When Pope Gregory XIII reformed the calendar:

  • Skipped: October 5-14, 1582
  • Purpose: Realign with solar year

Handling in Library

// Dates around transition are marked
'lc(11.18.3.9.18) -> g(15/10/1582 CE*: 2299161)' // ← Note the *
'lc(11.18.3.9.17) -> g(4/10/1582 CE*: 2299160)' // ← Note the *

// Julian calendar continues unaffected
'lc(11.18.3.9.18) -> j(15/10/1582 CE: 2299161)' // No gap

Real-World Examples

Classic Maya Inscriptions

Stela 29, Tikal (oldest LC inscription)

8.12.14.8.15  13 Men 3 Sip

Translation: 292 CE (July 6)

Temple of Inscriptions, Palenque

9.13.0.0.0  8 Ajaw 8 Wo

Translation: 692 CE (March 12)

Modern Date

13.0.0.0.0  4 Ajaw 3 K'ank'in

Translation: December 21, 2012 (famous "end date")

Using the Library

const factory = new FullDateFactory();

// Parse ancient inscription
const tikal = factory.parse('13 Men 3 Sip 8.12.14.8.15');

// Convert to modern date
console.log(tikal.lc.asGregorian()); // July 6, 292 CE

// Add context
tikal.setComment('Stela 29, Tikal - Oldest known Long Count inscription');

// Calculate time span
const modern = new LongCount(13, 0, 0, 0, 0);
const elapsed = modern.minus(tikal.lc).run();
console.log(elapsed.getTotalDays()); // Days between dates

Summary

The Maya calendar system implemented in this library demonstrates:

  1. Mathematical Sophistication: Interlinked cycles with coprime periods
  2. Absolute Chronology: Long Count spanning millennia
  3. Precision: Day-accurate across 5,000+ years
  4. Flexibility: Wildcards for partial/uncertain dates
  5. Interoperability: Conversions to Western calendars

The domain model faithfully represents these aspects while maintaining clean code through established design patterns.