System Overview

The Tibetan calendar is based on the Kālacakra Tantra's astronomical system, which itself derives from Indian siddhānta astronomy. The calculations determine dates through a series of interconnected algorithms that track the positions of the Sun and Moon.

The fundamental approach is to calculate two key values for any given date:

  1. True Weekday (gza' dag) — When a lunar day ends in solar time
  2. True Solar Longitude (nyi dag) — The Sun's position in the zodiac

From these, all other calendar elements—lunar mansions, yogas, karanas, and more—can be derived.

The Tibetan Number System

Tibetan astronomy uses a unique positional number system based on hierarchical divisions. Values are represented as arrays with 5 or 6 elements:

Position Name (Tibetan) Name (Sanskrit) Divisor Time Equivalent
0 Main unit 27 (zodiac) or 7 (week) Signs or days
1 chu tshod nāḍī 60 24 minutes
2 chu srang pala 60 24 seconds
3 dbugs prāṇa (breaths) 6 4 seconds
4 cha shas fractional 67 or 707 varies

For example, a weekday value of [5, 35, 36, 4, 160] breaks down as:

5 days (Thursday, since 0=Saturday)
+ 35 × 24 minutes = 840 minutes = 14 hours
+ 36 × 24 seconds = 864 seconds = 14.4 minutes  
+ 4 × 4 seconds = 16 seconds
+ 160/707 × (4/6 seconds) = ~0.15 seconds

This means the lunar day ends approximately 14 hours, 14 minutes, and 16 seconds after the start of Thursday (which begins at daybreak, around 5:00 AM).

Arithmetic Operations

Addition and subtraction must propagate carries/borrows through all positions:

function addGen(a, b, n1, n2) {
  // n1 = modulus for position 0 (27 for zodiac, 7 for weekday)
  // n2 = modulus for position 4 (67 for solar, 707 for weekday)
  
  let r = a[4] + b[4];
  result[4] = r % n2;
  
  r = a[3] + b[3] + Math.floor(r / n2);
  result[3] = r % 6;
  
  r = a[2] + b[2] + Math.floor(r / 6);
  result[2] = r % 60;
  
  r = a[1] + b[1] + Math.floor(r / 60);
  result[1] = r % 60;
  
  r = a[0] + b[0] + Math.floor(r / 60);
  result[0] = r % n1;
  
  return result;
}

True Month Calculation (zla dag)

The true month (ཟླ་དག, zla dag) is the count of synodic months from the epoch. This is calculated as:

// For Phugpa and most traditions:
a = 12 × (year - epochYear) + month - 3

// Calculate intercalation index
b = 2 × a + epochIntercalationIndex
intercalationIndex = b % 65
trueMonth = a + Math.floor(b / 65)

The intercalation index determines whether the month is intercalary (leap month). In the Phugpa system, indices 48 and 49 mark intercalary months.

KTC Reference

See "Kālacakra and the Tibetan Calendar", page 15-16, for the mathematical derivation.

Weekday Calculation (gza' dag)

The true weekday tells us exactly when a lunar day ends. This involves:

  1. Mean weekday (gza' bar) — Linear approximation
  2. Lunar anomaly correction (ril cha) — Accounts for Moon's elliptical orbit
  3. Solar equation correction — Accounts for Sun's apparent motion

Mean Weekday

// Monthly mean weekday increment
gzadm = [1, 31, 50, 0, 480, 0]

// Mean weekday for month
gzadru = (gzadm × trueMonth) + epochGzada

// Daily increment for lunar day
tsedm = [0, 59, 3, 4, 16, 0]

// Mean weekday for lunar day
tsebar = gzadru + (tsedm × lunarDay)

Lunar Anomaly

The lunar anomaly (རིལ་ཆ, ril cha) models the Moon's varying speed due to its elliptical orbit:

b = trueMonth + rilB  // rilB is epoch-dependent
a = 2 × trueMonth + rilA + Math.floor(b / 126)
rilcha = [a % 28, b % 126]

The anomaly is used with interpolation tables to compute the correction:

// Interpolation coefficients
gzabye = [5, 5, 4, 3, 2, 1, -1, -2, -3, -4, -5, -5, -5, 5]
gzadom = [5, 10, 15, 19, 22, 24, 25, 24, 22, 19, 15, 10, 5, 0]

rilpo = rilcha[0] + lunarDay
index = rilpo % 14
if (index === 0) index = 14

// Calculate adjustment
adjustment = interpolate(rilcha[1], lunarDay, gzabye[index-1])

KTC Reference

See pages 21-26 for lunar anomaly and weekday correction algorithms.

Solar Longitude (nyi dag)

The true solar longitude (ཉི་དག, nyi dag) determines the Sun's position in the 27 zodiac signs (each sign = 13°20').

Mean Solar Longitude

// Monthly mean solar increment
nyidm = [2, 10, 58, 1, 17, 0]

// Mean solar longitude for month
nyidru = (nyidm × trueMonth) + epochNyida

// Daily solar increment
nyilm = [0, 4, 21, 5, 43, 0]

// Mean solar longitude for lunar day
nyibar = nyidru + (nyilm × lunarDay)

Solar Equation

The equation of center corrects for the Sun's elliptical apparent motion:

// Solar equation coefficients
nyibye = [4, 1, 1, 4, 6, 6]
nyidom = [6, 10, 11, 10, 6, 0]

// Determine which quadrant
nyiwor = nyibar - [6, 45, 0, 0, 0]  // Subtract apogee offset
test = 60 × nyiwor[0] + nyiwor[1]

if (test >= 810) {
  nyidor = 1  // Second half of zodiac
  nyiwor = nyiwor - [13, 30, 0, 0, 0]
  test = 60 × nyiwor[0] + nyiwor[1]
}

// Calculate correction using interpolation
quadrant = Math.floor(test / 135)
correction = interpolate(test % 135, nyiwor, nyibye[quadrant-1], nyidom[quadrant-1])

// Apply correction
if (nyidor === 0) {
  nyidag = nyibar - correction
} else {
  nyidag = nyibar + correction
}

KTC Reference

See pages 31-35 for the solar equation derivation.

Intercalary Months

The Tibetan calendar maintains synchronization with the solar year through intercalary (leap) months. When the intercalation index falls within certain values, a month is doubled.

In the Phugpa system:

  • Index 48-49: The month is intercalary (ཟླ་ལྷག, zla lhag)
  • The first occurrence is the regular month
  • The second occurrence is the delayed/intercalary month

Different traditions handle intercalation differently:

  • Phugpa: Intercalary months indicated by negative month numbers
  • Tsurphu: Uses indices 0-1 for intercalation
  • Error Correction: Different threshold values

Duplicated & Omitted Days

Because lunar days are defined by the Moon's motion, not solar time, the correspondence between lunar and solar days is irregular:

Duplicated Days (lhag)

When a lunar day spans parts of two solar days, both solar days receive the same lunar day number. This happens when the true weekday advances by 2 instead of 1 between consecutive lunar days.

Omitted Days (chad)

When two consecutive lunar days both end within the same solar day, one lunar day number is skipped. This occurs when the Julian day doesn't change between consecutive lunar days.

// Detecting duplicated/omitted days
currentJD = calculateJulianDay(lunarDay)
previousJD = calculateJulianDay(lunarDay - 1)
nextJD = calculateJulianDay(lunarDay + 1)

if (currentJD === previousJD + 2) {
  // This day is duplicated (lhag)
}
if (currentJD === previousJD) {
  // Previous day is omitted (chad)
}

Our Implementation

Technology Choices

We ported Henning's C code to TypeScript for several reasons:

  • Type Safety: TypeScript catches errors in complex calculations
  • BigInt Support: Native arbitrary-precision integers for BCD operations
  • Browser Compatibility: Runs entirely client-side
  • Modern Tooling: Works with Astro, bundlers, and modern development workflows

Key Differences from Original

  1. BCD Replacement: The original used custom Binary Coded Decimal routines for arbitrary precision. We use JavaScript BigInt where precision is critical and standard Number where sufficient.
  2. Immutable Data: Our functions return new arrays instead of modifying global variables.
  3. Class-Based API: Wrapped in a TibetanCalendar class for easier usage and state management.

Verification

We verified our implementation against:

  • Output files generated by the original TCG (pl_2025.txt, etc.)
  • Published Tibetan calendars
  • Henning's book examples

Source Code

The complete TypeScript implementation is in src/lib/tibetan-calendar.ts. Comments reference both the original C source files and KTC page numbers.

Further Resources