"use strict"; /* FWK is free software. Copyright (c) 2022 Kevin Gut Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /** Provides access to FWK functionality*/ namespace FWK { type GregorianMonthNames = [ string, string, string, string, string, string, string, string, string, string, string, string ]; type FwkMonthNames = [ string, string, string, string, string, string, string, string, string, string, string, string, string ] type WeekdayNames = [ string, string, string, string, string, string, string ]; type MonthAsWeeks = [ [number, number, number, number], [number, number, number, number], [number, number, number, number], [number, number, number, number] ]; type FwkYear = { [index: string]: FwkMonth }; type FwkMonth = { firstWeek: number; firstDoy: number; weeks: MonthAsWeeks; hasExtraDay: boolean }; type FwkYearMap = { year: number; isLeapYear: boolean; //Always identical to ret["June"].hasExtraDay map: FwkYear; }; /** * Converts a single digit number into double digit number with a leading zero * * Negative numbers are treated as zero, numbers above 99 as 99. * Decimals are cut off. * @param v Number * @returns Number with at least two digits */ function d2(v: number): string { v = Math.min(99, Math.max(0, Math.floor(v)) | 0); return v < 10 ? "0" + v : String(v); } /** * Gets the names for months in the FWK * @param names Optional array with 12 names for months in gregorian calendar. Uses english names if not supplied * @returns FWK month names */ export function getMonths(names?: GregorianMonthNames): FwkMonthNames { if (names === null || names === undefined) { names = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ]; } if (!(names instanceof Array) || names.length !== 12) { throw new Error("Argument if supplied must be array with 12 entries"); } return names.slice(0, 6).concat(["Sol"]).concat(names.slice(6)) as FwkMonthNames; } /** * Gets english weekdays, starting at monday * @returns english weekdays, starting at monday */ export function getWeekdays(): WeekdayNames { return [ "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" ]; } /** * Gets a standard FWK month with 28 days numbered 1 to 28, * split across four weeks * @returns standard FWK month */ export function getStandardMonth():MonthAsWeeks { //Note: Do not cache this const days = Array.from({ length: 28 }).map((v, i) => i + 1); const weeks = [ days.splice(0, 7), days.splice(0, 7), days.splice(0, 7), days.splice(0, 7) ]; return weeks as MonthAsWeeks; } /** * Converts the given date object into an FWK date formatted as ISO 8601 date string with the time information * * Note1: Lacks time zone designator, and is only accurate to a second * Note2: fwk does not concerns itself with the time * @param dt Gregorian date object * @returns ISO 8601 FWK date string */ export function getDate(dt: Date): string { const year = dt.getFullYear(); const map = getYearMap(year).map; const doy = getDayOfYear(dt); const months = getMonths(); const month = months.filter(v => map[v].firstDoy <= doy).pop() ?? ""; return [year, d2(months.indexOf(month) + 1), d2(doy - map[month].firstDoy + 1)].join("-") + "T" + [dt.getHours(), dt.getMinutes(), dt.getSeconds()].map(d2).join(':'); } /** * Gets the mapping for a year in FWK * * Note: Calling this twice, with y=2000 and y=2001 gives you both possible answers * containing a leap day (2000) or not (2001) * @param y * @returns Mapping of an entire FWK year */ export function getYearMap(y:number): FwkYearMap { if (typeof (y) !== "number" || y === 0 || y !== Math.floor(y)) { throw ("Invalid year. Must be non-zero integer"); } //Leap year from gregorian calendar const isLeapYear = y % 4 === 0 && (y % 100 !== 0 || y % 400 === 0); const ret = {} as FwkYear; //Construct 13 months with a standard week layout getMonths().forEach(function (month, index) { ret[month] = { firstWeek: index * 4 + 1, firstDoy: index * 4 * 7 + 1, weeks: getStandardMonth(), hasExtraDay: false }; }); //Adjust months for leap years if (isLeapYear) { ret["June"].hasExtraDay = true; getMonths().forEach(function (month, index) { if (index > 5) { ++ret[month].firstDoy; } }); } //December always has an extra day ret["December"].hasExtraDay = true; return { year: +y, isLeapYear: isLeapYear, //Always identical to ret["June"].hasExtraDay map: ret }; } /** * Gets the day of year (doy) for a given date object * @param dt Date object * @returns Day of year. First day is 1, last day is 365 or 366 */ export function getDayOfYear(dt: Date):number { if (!(dt instanceof Date)) { throw "Date argument required"; } let day = 1; const start = new Date(dt.getTime()); start.setDate(1); start.setMonth(0); while (start.getTime() < dt.getTime()) { start.setDate(start.getDate() + 1); ++day; } return day; } }