Skip to content

Core Functions API Reference

Vigesimal Functions

maya_encoding.core.vigesimal

Core vigesimal (base-20) number system functions.

The Maya vigesimal system decomposes numbers into positions of value 1, 20, 400, 8000, ... Each position (digit 0-19) further decomposes into bars (value 5, max 3) and dots (value 1, max 4).

This module provides pure functions for conversion, with numpy vectorization for performance.

to_vigesimal(n, n_levels=None)

Convert a non-negative integer to a list of vigesimal digits (LSB first).

Parameters

n : int Non-negative integer to convert. n_levels : int or None Number of vigesimal levels. If None, auto-detected. Result is zero-padded or truncated to this length.

Returns

list[int] List of vigesimal digits, least significant first. Each digit is in range [0, 19].

Raises

ValueError If n is negative.

Examples

to_vigesimal(0) [0] to_vigesimal(19) [19] to_vigesimal(20) [0, 1] to_vigesimal(347) [7, 17] to_vigesimal(347, n_levels=4) [7, 17, 0, 0]

Source code in src/maya_encoding/core/vigesimal.py
def to_vigesimal(n: int, n_levels: int | None = None) -> list[int]:
    """Convert a non-negative integer to a list of vigesimal digits (LSB first).

    Parameters
    ----------
    n : int
        Non-negative integer to convert.
    n_levels : int or None
        Number of vigesimal levels. If None, auto-detected.
        Result is zero-padded or truncated to this length.

    Returns
    -------
    list[int]
        List of vigesimal digits, least significant first.
        Each digit is in range [0, 19].

    Raises
    ------
    ValueError
        If n is negative.

    Examples
    --------
    >>> to_vigesimal(0)
    [0]
    >>> to_vigesimal(19)
    [19]
    >>> to_vigesimal(20)
    [0, 1]
    >>> to_vigesimal(347)
    [7, 17]
    >>> to_vigesimal(347, n_levels=4)
    [7, 17, 0, 0]

    """
    if n < 0:
        raise ValueError(f"n must be non-negative, got {n}. Use utils.handle_negatives first.")

    if n_levels is None:
        n_levels = auto_n_levels(n)

    digits = []
    remaining = int(n)
    for _ in range(n_levels):
        digits.append(remaining % 20)
        remaining //= 20

    return digits

from_vigesimal(digits)

Convert a list of vigesimal digits (LSB first) back to an integer.

Parameters

digits : list[int] Vigesimal digits, least significant first. Each must be in [0, 19].

Returns

int The reconstructed integer.

Raises

ValueError If any digit is outside [0, 19].

Examples

from_vigesimal([7, 17]) 347 from_vigesimal([0, 1]) 20 from_vigesimal([0]) 0

Source code in src/maya_encoding/core/vigesimal.py
def from_vigesimal(digits: list[int]) -> int:
    """Convert a list of vigesimal digits (LSB first) back to an integer.

    Parameters
    ----------
    digits : list[int]
        Vigesimal digits, least significant first. Each must be in [0, 19].

    Returns
    -------
    int
        The reconstructed integer.

    Raises
    ------
    ValueError
        If any digit is outside [0, 19].

    Examples
    --------
    >>> from_vigesimal([7, 17])
    347
    >>> from_vigesimal([0, 1])
    20
    >>> from_vigesimal([0])
    0

    """
    result = 0
    for i, d in enumerate(digits):
        if not 0 <= d <= 19:
            raise ValueError(f"Vigesimal digit must be in [0, 19], got {d} at position {i}.")
        result += d * (20 ** i)
    return result

to_bars_dots(digit)

Decompose a vigesimal digit (0-19) into bars and dots.

In the Maya system: - Each bar represents 5 - Each dot represents 1 - Maximum: 3 bars (15) + 4 dots (4) = 19

Parameters

digit : int A vigesimal digit in range [0, 19].

Returns

tuple[int, int] (bars, dots) where bars in [0, 3] and dots in [0, 4].

Raises

ValueError If digit is outside [0, 19].

Examples

to_bars_dots(0) (0, 0) to_bars_dots(7) (1, 2) to_bars_dots(19) (3, 4)

Source code in src/maya_encoding/core/vigesimal.py
def to_bars_dots(digit: int) -> tuple[int, int]:
    """Decompose a vigesimal digit (0-19) into bars and dots.

    In the Maya system:
    - Each bar represents 5
    - Each dot represents 1
    - Maximum: 3 bars (15) + 4 dots (4) = 19

    Parameters
    ----------
    digit : int
        A vigesimal digit in range [0, 19].

    Returns
    -------
    tuple[int, int]
        (bars, dots) where bars in [0, 3] and dots in [0, 4].

    Raises
    ------
    ValueError
        If digit is outside [0, 19].

    Examples
    --------
    >>> to_bars_dots(0)
    (0, 0)
    >>> to_bars_dots(7)
    (1, 2)
    >>> to_bars_dots(19)
    (3, 4)

    """
    if not 0 <= digit <= 19:
        raise ValueError(f"Vigesimal digit must be in [0, 19], got {digit}.")
    return digit // 5, digit % 5

maya_decompose(n, n_levels=None)

Full Maya decomposition of a non-negative integer.

Returns the vigesimal digits, bars, and dots for each level.

Parameters

n : int Non-negative integer to decompose. n_levels : int or None Number of vigesimal levels. If None, auto-detected.

Returns

dict Dictionary with keys: - 'digits': list[int] — vigesimal digits (LSB first) - 'bars': list[int] — bars component per level - 'dots': list[int] — dots component per level - 'n_levels': int — number of levels used

Examples

maya_decompose(347) {'digits': [7, 17], 'bars': [1, 3], 'dots': [2, 2], 'n_levels': 2} maya_decompose(0) {'digits': [0], 'bars': [0], 'dots': [0], 'n_levels': 1}

Source code in src/maya_encoding/core/vigesimal.py
def maya_decompose(n: int, n_levels: int | None = None) -> dict:
    """Full Maya decomposition of a non-negative integer.

    Returns the vigesimal digits, bars, and dots for each level.

    Parameters
    ----------
    n : int
        Non-negative integer to decompose.
    n_levels : int or None
        Number of vigesimal levels. If None, auto-detected.

    Returns
    -------
    dict
        Dictionary with keys:
        - 'digits': list[int] — vigesimal digits (LSB first)
        - 'bars': list[int] — bars component per level
        - 'dots': list[int] — dots component per level
        - 'n_levels': int — number of levels used

    Examples
    --------
    >>> maya_decompose(347)
    {'digits': [7, 17], 'bars': [1, 3], 'dots': [2, 2], 'n_levels': 2}
    >>> maya_decompose(0)
    {'digits': [0], 'bars': [0], 'dots': [0], 'n_levels': 1}

    """
    digits = to_vigesimal(n, n_levels)
    bars = []
    dots = []
    for d in digits:
        b, dt = to_bars_dots(d)
        bars.append(b)
        dots.append(dt)

    return {
        "digits": digits,
        "bars": bars,
        "dots": dots,
        "n_levels": len(digits),
    }

auto_n_levels(max_value)

Calculate the minimum number of vigesimal levels needed to represent a value.

Parameters

max_value : int or float The maximum absolute value to represent.

Returns

int Number of vigesimal levels needed (minimum 1).

Examples

auto_n_levels(19) 1 auto_n_levels(20) 2 auto_n_levels(399) 2 auto_n_levels(400) 3

Source code in src/maya_encoding/core/vigesimal.py
def auto_n_levels(max_value: int | float) -> int:
    """Calculate the minimum number of vigesimal levels needed to represent a value.

    Parameters
    ----------
    max_value : int or float
        The maximum absolute value to represent.

    Returns
    -------
    int
        Number of vigesimal levels needed (minimum 1).

    Examples
    --------
    >>> auto_n_levels(19)
    1
    >>> auto_n_levels(20)
    2
    >>> auto_n_levels(399)
    2
    >>> auto_n_levels(400)
    3

    """
    if max_value <= 0:
        return 1
    abs_val = abs(int(max_value))
    if abs_val == 0:
        return 1
    return max(1, math.floor(math.log(abs_val) / math.log(20)) + 1)

maya_encode_array(values, n_levels, components='full', normalize=True)

Vectorized Maya encoding for a 1D array of non-negative integers.

Parameters

values : np.ndarray 1D array of non-negative integers. n_levels : int Number of vigesimal levels to use. components : str Which components to include: - 'full': digits, bars, and dots (3 features per level) - 'lite': digits only (1 feature per level) - 'bars_dots': bars and dots only (2 features per level) normalize : bool If True, normalize each component to [0, 1].

Returns

np.ndarray 2D array of shape (len(values), n_features). n_features = n_levels * features_per_level.

Source code in src/maya_encoding/core/vigesimal.py
def maya_encode_array(
    values: np.ndarray,
    n_levels: int,
    components: str = "full",
    normalize: bool = True,
) -> np.ndarray:
    """Vectorized Maya encoding for a 1D array of non-negative integers.

    Parameters
    ----------
    values : np.ndarray
        1D array of non-negative integers.
    n_levels : int
        Number of vigesimal levels to use.
    components : str
        Which components to include:
        - 'full': digits, bars, and dots (3 features per level)
        - 'lite': digits only (1 feature per level)
        - 'bars_dots': bars and dots only (2 features per level)
    normalize : bool
        If True, normalize each component to [0, 1].

    Returns
    -------
    np.ndarray
        2D array of shape (len(values), n_features).
        n_features = n_levels * features_per_level.

    """
    values = np.asarray(values, dtype=np.int64).ravel()
    n = len(values)

    # Compute vigesimal digits for all levels (vectorized)
    all_digits = np.zeros((n, n_levels), dtype=np.int64)
    remaining = values.copy()
    for level in range(n_levels):
        all_digits[:, level] = remaining % 20
        remaining //= 20

    # Compute bars and dots (vectorized)
    all_bars = all_digits // 5
    all_dots = all_digits % 5

    # Assemble output based on components
    if components == "lite":
        result = all_digits.astype(np.float64)
        if normalize:
            result /= 19.0
    elif components == "bars_dots":
        result = np.zeros((n, n_levels * 2), dtype=np.float64)
        for level in range(n_levels):
            result[:, level * 2] = all_bars[:, level]
            result[:, level * 2 + 1] = all_dots[:, level]
        if normalize:
            result[:, 0::2] /= 3.0  # bars: [0, 3]
            result[:, 1::2] /= 4.0  # dots: [0, 4]
    else:  # 'full'
        result = np.zeros((n, n_levels * 3), dtype=np.float64)
        for level in range(n_levels):
            result[:, level * 3] = all_digits[:, level]
            result[:, level * 3 + 1] = all_bars[:, level]
            result[:, level * 3 + 2] = all_dots[:, level]
        if normalize:
            result[:, 0::3] /= 19.0  # digits: [0, 19]
            result[:, 1::3] /= 3.0   # bars: [0, 3]
            result[:, 2::3] /= 4.0   # dots: [0, 4]

    return result

Calendar Functions

maya_encoding.core.calendar

Maya calendar conversion functions.

Implements conversions from Gregorian dates to the three Maya calendar systems: - Tzolk'in (260-day sacred calendar): 13 numbers x 20 day names - Haab' (365-day solar calendar): 18 months of 20 days + 5-day Wayeb' - Long Count (linear day count): mixed-radix system (20, 20, 18, 20, 20...)

All conversions go through Julian Day Number (JDN) as intermediate representation. Uses the GMT (Goodman-Martinez-Thompson) correlation constant (JDN 584283) by default.

gregorian_to_jdn(date)

Convert a Gregorian date to Julian Day Number (JDN).

Uses the standard algorithm for proleptic Gregorian calendar.

Parameters

date : DateLike The date to convert.

Returns

int Julian Day Number.

Examples

gregorian_to_jdn('2012-12-21') 2456283

Source code in src/maya_encoding/core/calendar.py
def gregorian_to_jdn(date: DateLike) -> int:
    """Convert a Gregorian date to Julian Day Number (JDN).

    Uses the standard algorithm for proleptic Gregorian calendar.

    Parameters
    ----------
    date : DateLike
        The date to convert.

    Returns
    -------
    int
        Julian Day Number.

    Examples
    --------
    >>> gregorian_to_jdn('2012-12-21')
    2456283

    """
    d = _parse_date(date)
    y = d.year
    m = d.month
    day = d.day

    # Standard JDN algorithm
    a = (14 - m) // 12
    y_adj = y + 4800 - a
    m_adj = m + 12 * a - 3

    jdn = (
        day
        + (153 * m_adj + 2) // 5
        + 365 * y_adj
        + y_adj // 4
        - y_adj // 100
        + y_adj // 400
        - 32045
    )
    return jdn

jdn_to_tzolkin(jdn, epoch_jdn=GMT_EPOCH_JDN)

Convert a Julian Day Number to Tzolk'in date.

The Tzolk'in is a 260-day cycle composed of two interlocking sub-cycles: - A number from 1 to 13 - A day name from 0 to 19 (index into TZOLKIN_DAY_NAMES)

Since gcd(13, 20) = 1, every combination occurs exactly once per 260 days.

Parameters

jdn : int Julian Day Number. epoch_jdn : int JDN of the Maya epoch (default: GMT correlation).

Returns

tuple[int, int] (number, day_name_index) where number in [1, 13] and day_name_index in [0, 19].

Examples

jdn_to_tzolkin(2456283) # 2012-12-21 = 4 Ajaw (4, 19)

Source code in src/maya_encoding/core/calendar.py
def jdn_to_tzolkin(jdn: int, epoch_jdn: int = GMT_EPOCH_JDN) -> tuple[int, int]:
    """Convert a Julian Day Number to Tzolk'in date.

    The Tzolk'in is a 260-day cycle composed of two interlocking sub-cycles:
    - A number from 1 to 13
    - A day name from 0 to 19 (index into TZOLKIN_DAY_NAMES)

    Since gcd(13, 20) = 1, every combination occurs exactly once per 260 days.

    Parameters
    ----------
    jdn : int
        Julian Day Number.
    epoch_jdn : int
        JDN of the Maya epoch (default: GMT correlation).

    Returns
    -------
    tuple[int, int]
        (number, day_name_index) where number in [1, 13] and day_name_index in [0, 19].

    Examples
    --------
    >>> jdn_to_tzolkin(2456283)  # 2012-12-21 = 4 Ajaw
    (4, 19)

    """
    # Days since epoch
    day_count = jdn - epoch_jdn

    # The epoch 0.0.0.0.0 corresponds to 4 Ajaw in the Tzolk'in
    # Ajaw = index 19, number = 4
    epoch_number = 4    # 1-indexed
    epoch_name = 19     # 0-indexed (Ajaw)

    # Tzolk'in number cycles through 1-13
    number = ((day_count + epoch_number - 1) % 13) + 1

    # Tzolk'in day name cycles through 0-19
    day_name = (day_count + epoch_name) % 20

    return number, day_name

jdn_to_haab(jdn, epoch_jdn=GMT_EPOCH_JDN)

Convert a Julian Day Number to Haab' date.

The Haab' is a 365-day cycle: - 18 months of 20 days each (months 0-17) - 1 short month "Wayeb'" of 5 days (month 18)

Parameters

jdn : int Julian Day Number. epoch_jdn : int JDN of the Maya epoch (default: GMT correlation).

Returns

tuple[int, int] (month_index, day) where month_index in [0, 18] and day in [0, 19] (or [0, 4] for Wayeb' month 18).

Examples

jdn_to_haab(2456283) # 2012-12-21 = 3 K'ank'in (13, 3)

Source code in src/maya_encoding/core/calendar.py
def jdn_to_haab(jdn: int, epoch_jdn: int = GMT_EPOCH_JDN) -> tuple[int, int]:
    """Convert a Julian Day Number to Haab' date.

    The Haab' is a 365-day cycle:
    - 18 months of 20 days each (months 0-17)
    - 1 short month "Wayeb'" of 5 days (month 18)

    Parameters
    ----------
    jdn : int
        Julian Day Number.
    epoch_jdn : int
        JDN of the Maya epoch (default: GMT correlation).

    Returns
    -------
    tuple[int, int]
        (month_index, day) where month_index in [0, 18] and day in [0, 19]
        (or [0, 4] for Wayeb' month 18).

    Examples
    --------
    >>> jdn_to_haab(2456283)  # 2012-12-21 = 3 K'ank'in
    (13, 3)

    """
    day_count = jdn - epoch_jdn

    # The epoch 0.0.0.0.0 corresponds to 8 Kumk'u in the Haab'
    # Kumk'u = month index 17, day 8
    # Day-in-year for 8 Kumk'u: 17*20 + 8 = 348
    epoch_haab_day = 17 * 20 + 8  # = 348

    # Position in the 365-day Haab' cycle
    haab_pos = (day_count + epoch_haab_day) % 365

    if haab_pos < 0:
        haab_pos += 365

    # First 360 days: 18 months of 20 days
    if haab_pos < 360:
        month = haab_pos // 20
        day = haab_pos % 20
    else:
        # Last 5 days: Wayeb'
        month = 18
        day = haab_pos - 360

    return month, day

jdn_to_long_count(jdn, n_levels=5, epoch_jdn=GMT_EPOCH_JDN)

Convert a Julian Day Number to Maya Long Count.

The Long Count is a mixed-radix system: - Level 0: kin (1 day) - Level 1: uinal (20 kin) - Level 2: tun (18 uinal = 360 days) ← calendar exception - Level 3: katun (20 tun = 7,200 days) - Level 4: baktun (20 katun = 144,000 days)

Parameters

jdn : int Julian Day Number. n_levels : int Number of Long Count levels to return (1-5). Default 5 (full). epoch_jdn : int JDN of the Maya epoch.

Returns

tuple[int, ...] Long Count digits from highest to lowest level. For n_levels=5: (baktun, katun, tun, uinal, kin).

Examples

jdn_to_long_count(2456283) # 2012-12-21 = 13.0.0.0.0 (13, 0, 0, 0, 0)

Source code in src/maya_encoding/core/calendar.py
def jdn_to_long_count(
    jdn: int, n_levels: int = 5, epoch_jdn: int = GMT_EPOCH_JDN
) -> tuple[int, ...]:
    """Convert a Julian Day Number to Maya Long Count.

    The Long Count is a mixed-radix system:
    - Level 0: kin (1 day)
    - Level 1: uinal (20 kin)
    - Level 2: tun (18 uinal = 360 days) ← calendar exception
    - Level 3: katun (20 tun = 7,200 days)
    - Level 4: baktun (20 katun = 144,000 days)

    Parameters
    ----------
    jdn : int
        Julian Day Number.
    n_levels : int
        Number of Long Count levels to return (1-5). Default 5 (full).
    epoch_jdn : int
        JDN of the Maya epoch.

    Returns
    -------
    tuple[int, ...]
        Long Count digits from highest to lowest level.
        For n_levels=5: (baktun, katun, tun, uinal, kin).

    Examples
    --------
    >>> jdn_to_long_count(2456283)  # 2012-12-21 = 13.0.0.0.0
    (13, 0, 0, 0, 0)

    """
    day_count = jdn - epoch_jdn

    # Decompose using mixed radix: kin, uinal, tun, katun, baktun
    digits = []
    remaining = day_count

    # Level 0: kin (mod 20)
    digits.append(remaining % 20)
    remaining //= 20

    # Level 1: uinal (mod 18) — the calendar exception
    digits.append(remaining % 18)
    remaining //= 18

    # Level 2+: tun, katun, baktun (all mod 20)
    for _ in range(n_levels - 2):
        digits.append(remaining % 20)
        remaining //= 20

    # Pad if needed
    while len(digits) < n_levels:
        digits.append(0)

    # Return in traditional order: highest level first
    return tuple(reversed(digits[:n_levels]))

is_wayeb(jdn, epoch_jdn=GMT_EPOCH_JDN)

Check if a Julian Day Number falls in the 5-day Wayeb' period.

Parameters

jdn : int Julian Day Number. epoch_jdn : int JDN of the Maya epoch.

Returns

bool True if the date is in Wayeb'.

Source code in src/maya_encoding/core/calendar.py
def is_wayeb(jdn: int, epoch_jdn: int = GMT_EPOCH_JDN) -> bool:
    """Check if a Julian Day Number falls in the 5-day Wayeb' period.

    Parameters
    ----------
    jdn : int
        Julian Day Number.
    epoch_jdn : int
        JDN of the Maya epoch.

    Returns
    -------
    bool
        True if the date is in Wayeb'.

    """
    month, _ = jdn_to_haab(jdn, epoch_jdn)
    return month == 18

dates_to_jdn_array(dates)

Convert an array of dates to Julian Day Numbers.

Parameters

dates : array-like Array of dates (strings, datetime, timestamps, or numpy datetime64).

Returns

np.ndarray 1D array of JDN integers.

Source code in src/maya_encoding/core/calendar.py
def dates_to_jdn_array(dates) -> np.ndarray:
    """Convert an array of dates to Julian Day Numbers.

    Parameters
    ----------
    dates : array-like
        Array of dates (strings, datetime, timestamps, or numpy datetime64).

    Returns
    -------
    np.ndarray
        1D array of JDN integers.

    """
    # Handle numpy datetime64 arrays efficiently
    if isinstance(dates, np.ndarray) and np.issubdtype(dates.dtype, np.datetime64):
        # Convert to days since epoch, then to JDN
        # JDN of 1970-01-01 = 2440588
        days_since_unix = (dates - np.datetime64("1970-01-01", "D")) / np.timedelta64(1, "D")
        return (days_since_unix + 2440588).astype(np.int64)

    # Handle pandas datetime
    try:
        import pandas as pd

        if isinstance(dates, pd.Series):
            if pd.api.types.is_datetime64_any_dtype(dates):
                days = (dates - pd.Timestamp("1970-01-01")).dt.days
                return (days.values + 2440588).astype(np.int64)
            else:
                dates = pd.to_datetime(dates)
                days = (dates - pd.Timestamp("1970-01-01")).dt.days
                return (days.values + 2440588).astype(np.int64)
    except ImportError:
        pass

    # Fallback: parse one by one
    result = np.array([gregorian_to_jdn(d) for d in dates], dtype=np.int64)
    return result