Skip to content

Repository files navigation

@drewsonne/maya-dates

Node CI npm version docs license

A typescript library for interacting with and modifying both the Maya Long Count (LC) and Calendar Round (CR) dates.

Table of Contents

Node.js Support

This library supports Node.js versions 20, 22, and 24 (LTS). Development is done on Node 24.

Quickstart

npm install @drewsonne/maya-dates
import { LongCountFactory } from '@drewsonne/maya-dates';

// Long Count from Initial Series on east side of Stele E, Quirigua
const lc = new LongCountFactory().parse('9.17.0.0.0');
console.log(`${lc.buildFullDate()}`);

This should print 13 Ajaw 18 Kumk'u 9.17. 0. 0. 0.

Instructions

Most implementations will consist of creating a fullDate, and passing it to an operation.

Date Creation

Creating a fullDate can be done either by:

  • using a factory class (LongCountFactory, CalendarRoundFactory, FullDateFactory) and calling its parse(raw_string) function, where raw_string is a LC, CR, or CR and LC combination encoded as a string. To specify missing values in a fullDate, using *. For example,
import { 
  Wildcard, 
  isWildcard,
  LongCountFactory,
  CalendarRoundFactory,
  FullDateFactory
} from '@drewsonne/maya-dates';

const wildcard = new Wildcard();

const partial_lc = new LongCountFactory().parse('9.17.0.0.*');
const partial_cr = new CalendarRoundFactory().parse('13 Ajaw * Kumk\'u');
const partial_date = new FullDateFactory().parse('13 Ajaw * Kumk\'u 9.17.0.0.*');

console.log(`LC: ${isWildcard(partial_lc.kIn)}`);
console.log(`CR: ${isWildcard(partial_cr.haab.coeff)}`);
console.log(`Full Date: ${isWildcard(partial_date.lc.kIn)}`);
  • passing explicit values into a model factory or class (getCalendarRound, LongCount, FullDate).
import {
  getCalendarRound,
  getHaab,
  getTzolkin,
  getHaabMonth,
  getTzolkinDay,
  coefficientParser as _,
  FullDate,
  LongCount
} from '@drewsonne/maya-dates';

const calendarRound = getCalendarRound(
    getTzolkin(_(13), getTzolkinDay('Ajaw')),
    getHaab(_(18), getHaabMonth('Kumk\'u'))
);
const lc = new LongCount(0, 0, 0, 17, 9);
const fullDate = new FullDate(calendarRound, lc);

console.log(`${lc}`);
console.log(`${calendarRound}`);
console.log(`${fullDate}`);

Operations

Once a full date object has been created, you can either add an integer to a fullDate using shift(number), or filling in missing values in wildcards. The operations module provides operators to expand a fullDate with a wildcard into all possible values for dates matching that wildcard pattern.

Common Patterns

Parsing Maya Dates

Parse different Maya date formats:

import { 
  LongCountFactory, 
  CalendarRoundFactory, 
  FullDateFactory 
} from '@drewsonne/maya-dates';

// Parse a Long Count date
const lcFactory = new LongCountFactory();
const longCount = lcFactory.parse('9.17.0.0.0');

// Parse a Calendar Round date
const crFactory = new CalendarRoundFactory();
const calendarRound = crFactory.parse('13 Ajaw 18 Kumk\'u');

// Parse a Full Date (Calendar Round + Long Count)
const fdFactory = new FullDateFactory();
const fullDate = fdFactory.parse('13 Ajaw 18 Kumk\'u 9.17.0.0.0');

console.log(`Long Count: ${longCount}`);
console.log(`Calendar Round: ${calendarRound}`);
console.log(`Full Date: ${fullDate}`);

Gregorian to Long Count Conversion

Convert between Gregorian dates and Maya Long Count:

import { LongCount, LongCountFactory } from '@drewsonne/maya-dates';

// Convert from JavaScript Date object
const date = new Date('2012-12-21');
const lc1 = LongCount.fromGregorian(date);
console.log(`Date object = ${lc1}`);
// Output: "Date object = 13. 0. 0. 0. 0"

// Convert from ISO 8601 date string
const lc2 = LongCount.fromGregorian('2012-12-21');
console.log(`ISO 8601 = ${lc2}`);
// Output: "ISO 8601 = 13. 0. 0. 0. 0"

// Alternative: Convert from Julian Day Number directly
const lc3 = LongCount.fromJulianDay(2456283);
console.log(`JDN 2456283 = ${lc3}`);
// Output: "JDN 2456283 = 13. 0. 0. 0. 0"

// Convert Long Count to Gregorian (reverse direction)
const lcFactory = new LongCountFactory();
const longCountDate = lcFactory.parse('13.0.0.0.0');
const gregorian = longCountDate.gregorian;
console.log(`Long Count ${longCountDate} = ${gregorian.day}/${gregorian.month}/${gregorian.year} ${gregorian.era}`);
// Output: "Long Count 13. 0. 0. 0. 0 = 21/12/2012 CE"

// Roundtrip conversion example
const original = lcFactory.parse('9.17.0.0.0');
const asGregorian = original.gregorian;
const backToLC = LongCount.fromJulianDay(asGregorian.julianDay);
console.log(`Original: ${original}, Roundtrip: ${backToLC}`);
// Both will be identical: " 9.17. 0. 0. 0"

Available conversion methods:

  • LongCount.fromGregorian(date | isoString, correlation?) - Convert from Date object or ISO 8601 date string (date only)
  • LongCount.fromJulianDay(jdn, correlation?) - Convert from Julian Day Number
  • LongCount.fromMayanDayNumber(mdn, correlation?) - Convert from Maya Day Number
  • longCount.gregorian - Convert Long Count to Gregorian (getter property)
  • longCount.julianDay - Get Julian Day Number for Long Count (getter property)

Working with Wildcards

Use wildcards to represent partial or unknown dates:

import { 
  LongCountFactory, 
  isWildcard,
  LongCountWildcard 
} from '@drewsonne/maya-dates';

// Create a partial date with a wildcard
const partialLc = new LongCountFactory().parse('9.17.0.0.*');

// Check if a component is a wildcard
if (isWildcard(partialLc.kIn)) {
  console.log('The k\'in position is a wildcard');
}

// Expand wildcards to get all possible dates
const wildcard = new LongCountWildcard(partialLc);
const allDates = wildcard.run();
console.log(`Found ${allDates.length} possible dates`);

Date Arithmetic

Perform arithmetic operations on Maya dates:

import { LongCountFactory, LongCount, DistanceNumber } from '@drewsonne/maya-dates';

const lc = new LongCountFactory().parse('9.17.0.0.0');

// Add days using plus() with a DistanceNumber
const oneDay = new DistanceNumber(1);
const nextDate = lc.plus(LongCount.fromDistanceNumber(oneDay)).equals();
console.log(`Next day: ${nextDate}`);

// Add multiple days
const manyDays = new DistanceNumber(365);
const futureDate = lc.plus(LongCount.fromDistanceNumber(manyDays)).equals();
console.log(`365 days later: ${futureDate}`);

// Subtract days using minus()
const hundredDays = new DistanceNumber(100);
const pastDate = lc.minus(LongCount.fromDistanceNumber(hundredDays)).equals();
console.log(`100 days earlier: ${pastDate}`);

// Get the full Calendar Round for a Long Count
const fullDate = lc.buildFullDate();
console.log(`Full date: ${fullDate}`);

// Shift Calendar Round dates
const shifted = fullDate.cr.shift(20);
console.log(`Calendar Round shifted by 20 days: ${shifted}`);

Alternative Spellings (i18n)

Support alternative spellings and transliterations of Maya calendar terms:

import { 
  CalendarRoundFactory, 
  getI18nManager, 
  LocaleDefinition 
} from '@drewsonne/maya-dates';

// Register alternative spellings
const i18n = getI18nManager();
const altSpellings: LocaleDefinition = {
  locale: 'alternative',
  name: 'Alternative Spellings',
  tzolkinDays: {
    'Ajaw': { canonical: 'Ajaw', alternatives: ['Ahau', 'Ajau'] }
  },
  haabMonths: {
    'Kumk\'u': { canonical: 'Kumk\'u', alternatives: ['Kumku', 'Cumku'] }
  }
};

i18n.registerLocale(altSpellings);

// Parse dates using alternative spellings
const factory = new CalendarRoundFactory();
const cr1 = factory.parse('4 Ahau 8 Kumku');      // Alternative
const cr2 = factory.parse('4 Ajaw 8 Kumk\'u');    // Canonical

// Both produce the same result
console.log(cr1.toString()); // "4 Ajaw 8 Kumk'u"
console.log(cr2.toString()); // "4 Ajaw 8 Kumk'u"

For more details, see I18n Documentation and the i18n example.

Migration Guide

Modern Import Syntax

This version introduces the exports field for better encapsulation and tree-shaking support.

Recommended: Named imports from package root

import { LongCountFactory, CalendarRoundFactory, FullDateFactory } from "@drewsonne/maya-dates";

Backwards Compatible (deprecated): Deep imports

// ⚠️ DEPRECATED: Still works for backwards compatibility
import LongCountFactory from "@drewsonne/maya-dates/lib/factory/long-count";
import CalendarRoundFactory from "@drewsonne/maya-dates/lib/factory/calendar-round";

Note: Deep imports via /lib/* are supported for backwards compatibility but may be removed in a future major version. Please migrate to named imports from the package root.

Benefits of Modern Imports

  • ✅ Better tree-shaking (smaller bundle sizes)
  • ✅ Cleaner, more maintainable code
  • ✅ Future-proof for ESM support
  • ✅ Consistent with modern npm package standards

Upgrading from v1.x

If you're upgrading from version 1.x, you'll need to update your import statements.

Before (v1.x):

import LongCountFactory from "@drewsonne/maya-dates/lib/factory/long-count";
import {Wildcard, isWildcard} from "@drewsonne/maya-dates/lib/wildcard";
import {getCalendarRound} from "@drewsonne/maya-dates/lib/cr/calendar-round";

After (v2.x):

import { 
  LongCountFactory, 
  Wildcard, 
  isWildcard,
  getCalendarRound 
} from '@drewsonne/maya-dates';

Key Changes:

  • All exports are now available from the package root (@drewsonne/maya-dates)
  • No need to specify /lib/ paths or know internal directory structure
  • Factory classes are now named exports instead of default exports
  • All class and function exports use named exports for better tree-shaking

Migration Steps:

  1. Replace all /lib/... imports with root-level imports
  2. Change default imports to named imports for factory classes
  3. Group related imports together in a single import statement
  4. Test your code to ensure all imports resolve correctly

Development

See docs/development/workflow.md for the development workflow and contribution guidelines.

Building

npm run build          # Compile TypeScript to JavaScript
npm run build:check    # Type check without emitting files

Testing

npm test               # Run all tests
npm run test:coverage  # Run tests with coverage report

Releasing

Releases are automated with release-please. Commits on main that follow Conventional Commits (fix:, feat:, feat!:/BREAKING CHANGE:) accumulate into an open chore(main): release x.y.z pull request that carries the version bump and the CHANGELOG entry.

Merging that pull request is the release: it creates the tag and the GitHub Release, then publishes to npm and GitHub Packages. Commits without a conventional prefix, and chore:/docs:/test: commits, do not on their own trigger a release, so dependency updates accumulate rather than each shipping a version.

.release-please-manifest.json records the last published version, which is what commits are diffed against; it is not necessarily the version currently in package.json.

Publishing to npm authenticates with trusted publishing (OIDC) rather than a long-lived token, so there is no NPM_TOKEN to rotate. The trusted publisher is configured on npmjs.com against this repository and the workflow filename release.yml; if publishing ever moves to a different workflow file, that setting has to move with it. The publish step deliberately sets no NODE_AUTH_TOKEN - setup-node writes _authToken=${NODE_AUTH_TOKEN} into .npmrc, and an empty value there makes npm skip the OIDC exchange and fail with ENEEDAUTH.

If a tag and GitHub Release exist but the npm publish failed, re-publish it by running the Release workflow manually with publish_tag set to that tag (for example v1.3.12) rather than cutting a new version.

release.yml is the only workflow that publishes. There is no NPM_TOKEN secret anywhere in the repository.

Documentation

The documentation site under website/ has its own dependencies. Install them once before running any of the docs: scripts:

npm run docs:install   # Install website dependencies (website/)
npm run docs:start     # Start Docusaurus dev server
npm run docs:build     # Build static documentation site
npm run docs:serve     # Serve built documentation locally

Documentation

Full documentation is built with Docusaurus and includes API reference generated from TypeDoc. You can browse the documentation at https://drewsonne.github.io/maya-dates/.

Releases

Packages

Used by

Contributors

Languages