chrono-40k
Version:
A consistent dating system for 'Warhammer 40,000'
639 lines (619 loc) • 19.5 kB
JavaScript
;
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