UNPKG

chrono-40k

Version:

A consistent dating system for 'Warhammer 40,000'

639 lines (619 loc) 19.5 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { Chrono: () => Chrono, Formats: () => Formats }); module.exports = __toCommonJS(index_exports); // src/chrono/common/common.utils.ts var MARCO_CONST = 1 / (365 * 24 / 1e3); var MARCO_CONST_LEAPYEAR = 1 / (366 * 24 / 1e3); var ONE_DAY = 1e3 * 60 * 60 * 24; function parseMillenium(milleniumValue) { milleniumValue = milleniumValue.substring(1); return parseInt(milleniumValue); } function isClassicImperialDate(date) { return typeof date === "object" && "check" in date; } function isIndomitusEraImperialDate(date) { return typeof date === "object" && "designator" in date; } function isTerranDate(date) { return date instanceof Date; } function isString(date) { return typeof date === "string"; } function getFraction(date) { const day = getDayOfYear(date); const hours = date.getUTCHours(); const determinedHour = day * 24 + hours; const year = date.getUTCFullYear(); let fraction = Math.trunc(determinedHour * getMarcoConstant(year)) + 1; if (fraction === 1e3) { fraction = 0; } return fraction; } function getYear(date) { return date.getUTCFullYear() % 1e3; } function getMillenium(date) { let millenium = date.getUTCFullYear() / 1e3; millenium += 1; return Math.trunc(millenium); } function getMarcoConstant(year) { return isLeapYear(year) ? MARCO_CONST_LEAPYEAR : MARCO_CONST; } function getDayOfYear(date) { const startOfYear = Date.UTC(date.getUTCFullYear(), 0, 0); const diff = date.getTime() - startOfYear; return Math.floor(diff / ONE_DAY) - 1; } function isLeapYear(year) { return year % 4 === 0 && year % 100 !== 0 || year % 400 === 0; } // src/converter/classic-to-terran.ts function convertClassicToTerranDate(date) { const { check, fraction, year, millenium } = date; if (check !== 0) { throw new Error( "Cannot directly convert from non-terran date to terran date" ); } const fullYear = (millenium - 1) * 1e3 + year; const marcoConstant = getMarcoConstant(fullYear); let determinedHour = (fraction - 1) / marcoConstant; if (fraction === 0) { determinedHour = 999 / marcoConstant; } const dayOfYear = Math.floor(determinedHour / 24); const hours = Math.floor(determinedHour % 24); const startOfYear = new Date(Date.UTC(fullYear, 0, 1)); startOfYear.setUTCDate(startOfYear.getUTCDate() + dayOfYear); startOfYear.setUTCHours(hours, 0, 0, 0); return startOfYear; } // src/converter/common/common.utils.ts function calculateDateDifference(from, to) { const minuend = getTotalFractions(from); const subtrahend = getTotalFractions(to); const difference = minuend - subtrahend; const years = Math.trunc(difference / 1e3); const fractions = difference % 1e3; return { years, fractions }; } function inferDate(totalFractions) { const check = 0; const fraction = totalFractions % 1e3; const year = Math.trunc(totalFractions % (1e3 * 1e3) / 1e3); const millenium = Math.trunc(totalFractions / (1e3 * 1e3)) + 1; return { check, fraction, year, millenium }; } function getTotalFractions({ fraction, year, millenium }) { return (fraction === 0 ? 1e3 : fraction) + year * 1e3 + (millenium - 1) * 1e3 * 1e3; } // src/converter/indomitus-to-classic.ts function convertIndomitusToClassicDate(date) { if (date.designator !== "T") { throw new Error( "Cannot infer date check for event that did not happen on holy terra" ); } const base = ClassicImperialCalendar.GREAT_RIFT_OPENING; let fraction = date.chronosegments; let years = date.annualDesignator; let millenia = -(base.millenium - date.millenium); if (!date.isPostGreatRift) { fraction = -fraction; years = -years; } else { millenia = millenia - 1; } const targetTotalFractions = millenia * 1e3 * 1e3 + years * 1e3 + fraction; const baseTotalFractions = getTotalFractions(base); const totalFractions = baseTotalFractions + targetTotalFractions; return inferDate(totalFractions); } // src/converter/terran-to-classic.ts function convertTerranToClassicDate(date) { const check = 0; const fraction = getFraction(date); const year = getYear(date); const millenium = getMillenium(date); return { check, fraction, year, millenium }; } // src/formatter/formatter.ts var ImperialDateFormatter = class { constructor(pattern, segments) { this.pattern = pattern; this.segments = segments; } format(date) { let formattedDate = this.pattern; for (const { symbol, format } of this.segments) { formattedDate = formattedDate.replace( new RegExp(symbol, "g"), format(date) ); } return formattedDate; } }; // src/parser/parser.utils.ts function getFormatExpression(segments) { return (pattern) => { let expression = escapeControlSequences(pattern); const placeholders = {}; for (const [index, segment] of segments.entries()) { const replacement = segment.expression; const placeholder = `_PLACEHOLDER_${index}_`; placeholders[placeholder] = replacement; expression = expression.replace( new RegExp(segment.symbol, "g"), placeholder ); } for (const [placeholder, replacement] of Object.entries(placeholders)) { expression = expression.replace( new RegExp(placeholder, "g"), replacement ); } return new RegExp(`^${expression}$`); }; } function bySymbol(symbol) { return (segment) => segment.symbol === symbol; } function escapeControlSequences(pattern) { return pattern.replace(/\(/g, "\\(").replace(/\)/g, "\\)").replace(/\[/g, "\\[").replace(/\]/g, "\\]").replace(/\{/g, "\\{").replace(/\}/g, "\\}"); } // src/parser/parser.ts var ImperialDateParser = class { constructor(formats, segments, dateFactory, dateConverter) { this.segments = segments; this.dateFactory = dateFactory; this.dateConverter = dateConverter; this.formatExpressions = formats.map(getFormatExpression(this.segments)); } parse(date) { for (let regex of this.formatExpressions) { const execArray = regex.exec(date); if (!(execArray && execArray.groups)) { continue; } return this.match(execArray.groups); } const terranDate = new Date(date); if (terranDate instanceof Date && isNaN(terranDate.getTime())) { throw new Error("Date did not match any expected pattern"); } return this.dateConverter(terranDate); } match(captureGroups) { const imperialDate = this.dateFactory(); for (const [symbol, value] of Object.entries(captureGroups)) { const segment = this.segments.find(bySymbol(symbol)); if (!segment) { continue; } segment.parse(imperialDate, value); } return imperialDate; } }; // src/chrono/chrono.model.ts var Formats = /* @__PURE__ */ ((Formats2) => { Formats2["CLASSIC_DEFAULT_FORMAT"] = "c fff yyy.m"; Formats2["CLASSIC_SHORT_FORMAT"] = "yyy.m"; Formats2["INDOMITUS_ERA_DEFAULT_FORMAT"] = "y.f gg d.m"; Formats2["INDOMITUS_ERA_SHORT_FORMAT"] = "y.fg d.m"; return Formats2; })(Formats || {}); // src/chrono/classic/classic-calendar.utils.ts function classicImperialDateFactory() { return { check: 0, fraction: 1, year: 0, millenium: 0 }; } function parseCheck(checkValue) { const check = parseInt(checkValue); if (check < 0 || 9 < check) { throw new Error(`check ${check} is out of bounds`); } return check; } function parseYearFraction(fractionValue) { return parseInt(fractionValue); } function parseYear(yearValue) { return parseInt(yearValue); } // src/chrono/classic/classic-calendar.model.ts var classicImperialDateSegments = [ { symbol: "c", expression: "(?<c>\\d)", format: (date) => `${date.check}`, parse: (date, check) => date.check = parseCheck(check) }, { symbol: "fff", expression: "(?<fff>\\d{3})", format: (date) => `${date.fraction}`.padStart(3, "0"), parse: (date, fraction) => date.fraction = parseYearFraction(fraction) }, { symbol: "ff", expression: "(?<ff>\\d{2,3})", format: (date) => `${date.fraction}`.padStart(2, "0"), parse: (date, fraction) => date.fraction = parseYearFraction(fraction) }, { symbol: "f", expression: "(?<f>\\d{1,3})".padStart(1, "0"), format: (date) => `${date.fraction}`, parse: (date, fraction) => date.fraction = parseYearFraction(fraction) }, { symbol: "yyy", expression: "(?<yyy>\\d{3})", format: (date) => `${date.year}`.padStart(3, "0"), parse: (date, year) => date.year = parseYear(year) }, { symbol: "yy", expression: "(?<yy>\\d{2,3})", format: (date) => `${date.year}`.padStart(2, "0"), parse: (date, year) => date.year = parseYear(year) }, { symbol: "y", expression: "(?<y>\\d{1,3})", format: (date) => `${date.year}`.padStart(1, "0"), parse: (date, year) => date.year = parseYear(year) }, { symbol: "m", expression: "(?<m>M\\d+)", format: (date) => `M${date.millenium}`.padEnd(2, "0"), parse: (date, millenium) => date.millenium = parseMillenium(millenium) } ]; // src/chrono/classic/classic-calendar.ts var _ClassicImperialCalendar = class _ClassicImperialCalendar { parse(date, format) { return this.internalParse(date, format); } format(date, format) { const proxy = this.internalParse(date, format); if (typeof format === "undefined") { format = "c fff yyy.m" /* CLASSIC_DEFAULT_FORMAT */; } const formatter = new ImperialDateFormatter( format, classicImperialDateSegments ); return formatter.format(proxy); } isValid(date) { const { check, fraction } = date; if (check < 0 || 9 < check) { return false; } if (fraction < 0 || 999 < fraction) { return false; } return true; } getTerranDate(date, format) { const proxy = this.internalParse(date, format); return convertClassicToTerranDate(proxy); } internalParse(date, format) { let classicDate = void 0; if (isString(date)) { classicDate = this.fromString(date, format); } if (isClassicImperialDate(date)) { classicDate = this.fromClassicImperialDate(date); } if (isIndomitusEraImperialDate(date)) { classicDate = this.fromIndomitusEraDate(date); } if (isTerranDate(date)) { classicDate = this.fromTerranDate(date); } if (!classicDate) { throw new Error("Type mismatch on input"); } return { ...classicDate, toString: (format2) => this.format(classicDate, format2), toDate: () => this.getTerranDate(classicDate) }; } fromString(date, format) { const formats = [ "c fff yyy.m" /* CLASSIC_DEFAULT_FORMAT */.valueOf(), "yyy.m" /* CLASSIC_SHORT_FORMAT */.valueOf() ]; if (format) { formats.push(format); } const parser = new ImperialDateParser( formats, classicImperialDateSegments, classicImperialDateFactory, convertTerranToClassicDate ); return parser.parse(date); } fromTerranDate(date) { return convertTerranToClassicDate(date); } fromClassicImperialDate(date) { if (!this.isValid(date)) { throw new Error("Invalid date"); } return { ...date }; } fromIndomitusEraDate(date) { return convertIndomitusToClassicDate(date); } }; _ClassicImperialCalendar.GREAT_RIFT_OPENING = new _ClassicImperialCalendar().parse( "0 000 999.M41" ); var ClassicImperialCalendar = _ClassicImperialCalendar; // src/chrono/indomitus-era/indomitus-era-calendar.utils.ts function indomitusEraImperialDateFactory() { return { designator: "T", chronosegments: 1, annualDesignator: 0, millenium: 0, isPostGreatRift: true }; } function parseChronosegments(chronosegmentsValue) { return parseInt(chronosegmentsValue); } function parseAnnualDesignator(annualDesignartorValue) { return parseInt(annualDesignartorValue); } function parseDesignator(designatorValue) { return designatorValue.slice(0, -2).toLocaleUpperCase(); } // src/converter/classic-to-indomitus.ts function convertClassicToIndomitusDate(date) { if (date.check !== 0) { throw new Error( "Cannot infer designator for other location than holy terra" ); } const base = ClassicImperialCalendar.GREAT_RIFT_OPENING; const dateDiff = calculateDateDifference(base, date); const indomitusEraDate = indomitusEraImperialDateFactory(); const isPostGreatRift = dateDiff.years < 0 || dateDiff.fractions < 0; indomitusEraDate.annualDesignator = Math.abs(dateDiff.years % 1e3); indomitusEraDate.chronosegments = Math.abs(dateDiff.fractions); indomitusEraDate.isPostGreatRift = isPostGreatRift; indomitusEraDate.millenium = Math.trunc( base.millenium - (dateDiff.years / 1e3 - 1) ); return indomitusEraDate; } // src/converter/indomitus-to-terran.ts function convertIndomitusToTerranDate(date) { const classicDate = convertIndomitusToClassicDate(date); return convertClassicToTerranDate(classicDate); } // src/converter/terran-to-indomitus.ts function convertTerranToIndomitusDate(date) { const classicDate = convertTerranToClassicDate(date); return convertClassicToIndomitusDate(classicDate); } // src/chrono/indomitus-era/indomitus-era-calendar.model.ts var indomitusEraImperialDateSegments = [ { symbol: "d", expression: "(?<d>\\w+CM)", format: (date) => `${date.designator}CM`, parse: (date, designator) => date.designator = parseDesignator(designator) }, { symbol: "fff", expression: "(?<fff>\\d{3})", format: (date) => `${date.chronosegments}`.padStart(3, "0"), parse: (date, chronosegments) => date.chronosegments = parseChronosegments(chronosegments) }, { symbol: "ff", expression: "(?<ff>\\d{2,3})", format: (date) => `${date.chronosegments}`.padStart(2, "0"), parse: (date, chronosegments) => date.chronosegments = parseChronosegments(chronosegments) }, { symbol: "f", expression: "(?<f>\\d{1,3})", format: (date) => `${date.chronosegments}`.padStart(1, "0"), parse: (date, chronosegments) => date.chronosegments = parseChronosegments(chronosegments) }, { symbol: "yyy", expression: "(?<yyy>\\d{3})", format: (date) => `${date.annualDesignator}`.padStart(3, "0"), parse: (date, annualDesignator) => date.annualDesignator = parseAnnualDesignator(annualDesignator) }, { symbol: "yy", expression: "(?<yy>\\d{2,3})", format: (date) => `${date.annualDesignator}`.padStart(2, "0"), parse: (date, annualDesignator) => date.annualDesignator = parseAnnualDesignator(annualDesignator) }, { symbol: "y", expression: "(?<y>\\d{1,3})", format: (date) => `${date.annualDesignator}`.padStart(1, "0"), parse: (date, annualDesignator) => date.annualDesignator = parseAnnualDesignator(annualDesignator) }, { symbol: "m", expression: "(?<m>M\\d+)", format: (date) => `M${date.millenium}`.padEnd(2, "0"), parse: (date, millenium) => date.millenium = parseMillenium(millenium) }, { symbol: "gg", expression: "(?<gg>(post|previo))", format: (date) => date.isPostGreatRift ? "post" : "previo", parse: (date, timeDesignator) => date.isPostGreatRift = timeDesignator === "post" }, { symbol: "g", expression: "(?<g>[+-])", format: (date) => date.isPostGreatRift ? "+" : "-", parse: (date, timeDesignator) => date.isPostGreatRift = timeDesignator === "+" } ]; // src/chrono/indomitus-era/indomitus-era-calendar.ts var IndomitusEraImperialCalendar = class { parse(date, format) { return this.internalParse(date, format); } format(date, format) { const proxy = this.internalParse(date); if (typeof format === "undefined") { format = "y.f gg d.m" /* INDOMITUS_ERA_DEFAULT_FORMAT */; } const formatter = new ImperialDateFormatter( format, indomitusEraImperialDateSegments ); return formatter.format(proxy); } isValid(date) { const { designator, chronosegments } = date; if (designator === "") { return false; } if (chronosegments < 0 || 999 < chronosegments) { return false; } return true; } getTerranDate(date, format) { const proxy = this.internalParse(date, format); return convertIndomitusToTerranDate(proxy); } internalParse(date, format) { let indomitusEraDate = void 0; if (isString(date)) { indomitusEraDate = this.fromString(date, format); } if (isIndomitusEraImperialDate(date)) { indomitusEraDate = this.fromIndomitusEraImperialDate(date); } if (isTerranDate(date)) { indomitusEraDate = this.fromTerranDate(date); } if (isClassicImperialDate(date)) { indomitusEraDate = this.fromClassicImperialDate(date); } if (!indomitusEraDate) { throw new Error("Type mismatch on input"); } return { ...indomitusEraDate, toString: (format2) => this.format(indomitusEraDate, format2), toDate: () => this.getTerranDate(indomitusEraDate) }; } fromString(date, format) { const formats = [ "y.f gg d.m" /* INDOMITUS_ERA_DEFAULT_FORMAT */.valueOf(), "y.fg d.m" /* INDOMITUS_ERA_SHORT_FORMAT */.valueOf() ]; if (format) { formats.push(format); } const parser = new ImperialDateParser( formats, indomitusEraImperialDateSegments, indomitusEraImperialDateFactory, convertTerranToIndomitusDate ); return parser.parse(date); } fromTerranDate(date) { return convertTerranToIndomitusDate(date); } fromIndomitusEraImperialDate(date) { if (!this.isValid(date)) { throw new Error("Invalid date"); } return { ...date }; } fromClassicImperialDate(date) { return convertClassicToIndomitusDate(date); } }; // src/chrono/chrono.ts var _Chrono = class _Chrono { }; /** * Calendar for handling dates in the old style (pre-great rift) of the imperial dating system * * E. g.: * - "0 000 999.M41" * - "005.M30" */ _Chrono.classic = new ClassicImperialCalendar(); /** * Calendar for handling dates in the old style (post-great rift) of the imperial dating system * * E. g.: * - 0.1 previo VCM.M41 * - 1.1+ TCM.M41 */ _Chrono.indomitus = new IndomitusEraImperialCalendar(); _Chrono.parse = _Chrono.classic.parse.bind(_Chrono.classic); _Chrono.format = _Chrono.classic.format.bind(_Chrono.classic); _Chrono.valid = _Chrono.classic.isValid.bind(_Chrono.classic); _Chrono.getTerranDate = _Chrono.classic.getTerranDate.bind(_Chrono.classic); var Chrono = _Chrono; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Chrono, Formats }); //# sourceMappingURL=index.js.map