universal-common
Version:
Library that provides useful missing base class library functionality.
791 lines (702 loc) • 30.8 kB
JavaScript
import ArgumentError from './ArgumentError.js';
import DateTime from './DateTime.js';
import DateTimeKind from './DateTimeKind.js';
import TimeSpan from './TimeSpan.js';
import DateTimeFormatInfo from './DateTimeFormatInfo.js';
/**
* Provides functionality to format DateTime instances according to format strings.
* This is the main formatting engine that processes custom format patterns.
*/
export default class DateTimeFormat {
// Maximum number of fractional second digits supported
static MAX_SECONDS_FRACTION_DIGITS = 7;
// Constant representing null offset for DateTime (vs DateTimeOffset)
static NULL_OFFSET = Number.MIN_SAFE_INTEGER;
// All standard format characters
static ALL_STANDARD_FORMATS = "dDfFgGmMoOrRstTuUyY";
// Standard format patterns
static ROUNDTRIP_FORMAT = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.fffffffK";
static ROUNDTRIP_DATETIME_UNFIXED = "yyyy'-'MM'-'ddTHH':'mm':'ss zzz";
// Format length constants
static FORMAT_O_MIN_LENGTH = 27;
static FORMAT_O_MAX_LENGTH = 33;
static FORMAT_INVARIANT_G_MIN_LENGTH = 19;
static FORMAT_INVARIANT_G_MAX_LENGTH = 26;
static FORMAT_R_LENGTH = 29;
static FORMAT_S_LENGTH = 19;
static FORMAT_U_LENGTH = 20;
// Fixed number formats for fractional seconds
static FIXED_NUMBER_FORMATS = [
"0",
"00",
"000",
"0000",
"00000",
"000000",
"0000000"
];
// Invariant format info cache
static #invariantFormatInfo = null;
// Invariant abbreviated month/day names
static #invariantAbbreviatedMonthNames = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec", ""
];
static #invariantAbbreviatedDayNames = [
"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
];
/**
* Gets the invariant DateTimeFormatInfo instance.
*
* @type {DateTimeFormatInfo}
* @readonly
* @static
*/
static get invariantFormatInfo() {
if (!DateTimeFormat.#invariantFormatInfo) {
DateTimeFormat.#invariantFormatInfo = DateTimeFormatInfo.invariantInfo;
}
return DateTimeFormat.#invariantFormatInfo;
}
/**
* Formats a positive integer with leading zeros.
*
* @private
* @param {number} value - The value to format
* @param {number} minimumLength - Minimum length with leading zeros
* @returns {string} Formatted number string
*/
static #formatDigits(value, minimumLength) {
if (value < 0) {
throw new ArgumentError("DateTimeFormat.formatDigits(): value must be >= 0");
}
return value.toString().padStart(minimumLength, '0');
}
/**
* Parses the repeat count of a pattern character.
*
* @private
* @param {string} format - The format string
* @param {number} pos - Starting position
* @param {string} patternChar - The character to count
* @returns {number} Number of consecutive pattern characters
*/
static #parseRepeatPattern(format, pos, patternChar) {
let index = pos + 1;
while (index < format.length && format[index] === patternChar) {
index++;
}
return index - pos;
}
/**
* Formats day of week name.
*
* @private
* @param {number} dayOfWeek - Day of week (0-6, Sunday=0)
* @param {number} repeat - Number of pattern characters
* @param {DateTimeFormatInfo} dtfi - Format info
* @returns {string} Formatted day name
*/
static #formatDayOfWeek(dayOfWeek, repeat, dtfi) {
if (dayOfWeek < 0 || dayOfWeek > 6) {
throw new ArgumentError("Day of week out of range");
}
if (repeat === 3) {
return dtfi.getAbbreviatedDayName(dayOfWeek);
}
return dtfi.getDayName(dayOfWeek);
}
/**
* Formats month name.
*
* @private
* @param {number} month - Month (1-12)
* @param {number} repeatCount - Number of pattern characters
* @param {DateTimeFormatInfo} dtfi - Format info
* @returns {string} Formatted month name
*/
static #formatMonth(month, repeatCount, dtfi) {
if (month < 1 || month > 12) {
throw new ArgumentError("Month out of range");
}
if (repeatCount === 3) {
return dtfi.getAbbreviatedMonthName(month);
}
return dtfi.getMonthName(month);
}
/**
* Parses quoted string in format pattern.
*
* @private
* @param {string} format - The format string
* @param {number} pos - Position of quote character
* @returns {{text: string, length: number}} Parsed text and consumed length
*/
static #parseQuoteString(format, pos) {
const formatLen = format.length;
const beginPos = pos;
const quoteChar = format[pos++];
let foundQuote = false;
let result = '';
while (pos < formatLen) {
const ch = format[pos++];
if (ch === quoteChar) {
foundQuote = true;
break;
} else if (ch === '\\') {
if (pos < formatLen) {
result += format[pos++];
} else {
throw new ArgumentError("Invalid format string - backslash at end");
}
} else {
result += ch;
}
}
if (!foundQuote) {
throw new ArgumentError(`Invalid format string - unmatched quote: ${quoteChar}`);
}
return {
text: result,
length: pos - beginPos
};
}
/**
* Gets the next character in format string.
*
* @private
* @param {string} format - The format string
* @param {number} pos - Current position
* @returns {number} Next character code, or -1 if at end
*/
static #parseNextChar(format, pos) {
if (pos + 1 >= format.length) {
return -1;
}
return format.charCodeAt(pos + 1);
}
/**
* Checks if genitive form should be used for month names.
*
* @private
* @param {string} format - The format string
* @param {number} index - Current position
* @param {number} tokenLen - Length of current token
* @param {string} patternToMatch - Pattern to search for (usually 'd')
* @returns {boolean} Whether to use genitive form
*/
static #isUseGenitiveForm(format, index, tokenLen, patternToMatch) {
// Look back for day pattern
let i = index - 1;
let repeat = 0;
// Find first occurrence of pattern
while (i >= 0 && format[i] !== patternToMatch) {
i--;
}
if (i >= 0) {
// Count consecutive patterns
while (--i >= 0 && format[i] === patternToMatch) {
repeat++;
}
// repeat == 0 means one pattern, repeat == 1 means two patterns
if (repeat <= 1) {
return true;
}
}
// Look ahead for day pattern
i = index + tokenLen;
while (i < format.length && format[i] !== patternToMatch) {
i++;
}
if (i < format.length) {
repeat = 0;
while (++i < format.length && format[i] === patternToMatch) {
repeat++;
}
if (repeat <= 1) {
return true;
}
}
return false;
}
/**
* Formats fractional seconds.
*
* @private
* @param {number} fraction - Fractional part
* @param {string} fractionFormat - Format pattern
* @returns {string} Formatted fraction
*/
static #formatFraction(fraction, fractionFormat) {
return DateTimeFormat.#formatDigits(fraction, fractionFormat.length);
}
/**
* Formats timezone offset.
*
* @private
* @param {DateTime} dateTime - The datetime
* @param {TimeSpan|number} offset - Offset from UTC (or NULL_OFFSET)
* @param {number} tokenLen - Length of 'z' pattern
* @param {boolean} timeOnly - Whether this is time-only formatting
* @returns {string} Formatted timezone
*/
static #formatCustomizedTimeZone(dateTime, offset, tokenLen, timeOnly) {
let offsetSpan;
let dateTimeFormat = (offset === DateTimeFormat.NULL_OFFSET);
if (dateTimeFormat) {
// No offset provided - determine from DateTime
if (timeOnly && dateTime.ticks < TimeSpan.TICKS_PER_DAY) {
// For time-only, use current system offset
const now = new Date();
offsetSpan = TimeSpan.fromMinutes(-now.getTimezoneOffset());
} else if (dateTime.kind === DateTimeKind.UTC) {
offsetSpan = TimeSpan.zero;
} else {
// Get local offset for the datetime
const jsDate = dateTime.toDate();
offsetSpan = TimeSpan.fromMinutes(-jsDate.getTimezoneOffset());
}
} else {
offsetSpan = offset instanceof TimeSpan ? offset : TimeSpan.fromTicks(offset);
}
let result = '';
const totalMinutes = offsetSpan.totalMinutes;
if (totalMinutes >= 0) {
result += '+';
} else {
result += '-';
}
const absMinutes = Math.abs(totalMinutes);
const hours = Math.floor(absMinutes / 60);
const minutes = absMinutes % 60;
if (tokenLen <= 1) {
// 'z' format: +8 or -7
result += hours.toString();
} else if (tokenLen === 2) {
// 'zz' format: +08 or -07
result += DateTimeFormat.#formatDigits(hours, 2);
} else {
// 'zzz' format: +08:00 or -07:30
result += DateTimeFormat.#formatDigits(hours, 2);
result += ':';
result += DateTimeFormat.#formatDigits(minutes, 2);
}
return result;
}
/**
* Formats roundtrip timezone (K format).
*
* @private
* @param {DateTime} dateTime - The datetime
* @param {TimeSpan|number} offset - Offset from UTC (or NULL_OFFSET)
* @returns {string} Formatted timezone for roundtrip
*/
static #formatCustomizedRoundtripTimeZone(dateTime, offset) {
if (offset === DateTimeFormat.NULL_OFFSET) {
// Source is DateTime
switch (dateTime.kind) {
case DateTimeKind.LOCAL:
const jsDate = dateTime.toDate();
const offsetMinutes = -jsDate.getTimezoneOffset();
const offsetSpan = TimeSpan.fromMinutes(offsetMinutes);
return DateTimeFormat.#formatCustomizedTimeZone(dateTime, offsetSpan, 3, false);
case DateTimeKind.UTC:
return 'Z';
default: // Unspecified
return '';
}
}
// Source is DateTimeOffset
const offsetSpan = offset instanceof TimeSpan ? offset : TimeSpan.fromTicks(offset);
return DateTimeFormat.#formatCustomizedTimeZone(dateTime, offsetSpan, 3, false);
}
/**
* Main formatting method for custom patterns.
*
* @private
* @param {DateTime} dateTime - The datetime to format
* @param {string} format - Custom format pattern
* @param {DateTimeFormatInfo} dtfi - Format info
* @param {TimeSpan|number} offset - Timezone offset (or NULL_OFFSET)
* @returns {string} Formatted datetime string
*/
static #formatCustomized(dateTime, format, dtfi, offset) {
let result = '';
let isTimeOnly = true;
let i = 0;
while (i < format.length) {
const ch = format[i];
let tokenLen;
let hour12;
switch (ch) {
case 'g':
tokenLen = DateTimeFormat.#parseRepeatPattern(format, i, ch);
result += dtfi.getEraName(dateTime);
break;
case 'h':
tokenLen = DateTimeFormat.#parseRepeatPattern(format, i, ch);
hour12 = dateTime.hour;
if (hour12 > 12) {
hour12 -= 12;
} else if (hour12 === 0) {
hour12 = 12;
}
result += DateTimeFormat.#formatDigits(hour12, Math.min(tokenLen, 2));
break;
case 'H':
tokenLen = DateTimeFormat.#parseRepeatPattern(format, i, ch);
result += DateTimeFormat.#formatDigits(dateTime.hour, Math.min(tokenLen, 2));
break;
case 'm':
tokenLen = DateTimeFormat.#parseRepeatPattern(format, i, ch);
result += DateTimeFormat.#formatDigits(dateTime.minute, Math.min(tokenLen, 2));
break;
case 's':
tokenLen = DateTimeFormat.#parseRepeatPattern(format, i, ch);
result += DateTimeFormat.#formatDigits(dateTime.second, Math.min(tokenLen, 2));
break;
case 'f':
case 'F':
tokenLen = DateTimeFormat.#parseRepeatPattern(format, i, ch);
if (tokenLen <= DateTimeFormat.MAX_SECONDS_FRACTION_DIGITS) {
const totalTicks = Number(dateTime.ticks);
const secondTicks = totalTicks % TimeSpan.TICKS_PER_SECOND;
const divisor = Math.pow(10, DateTimeFormat.MAX_SECONDS_FRACTION_DIGITS - tokenLen);
let fraction = Math.floor(secondTicks / divisor);
if (ch === 'f') {
result += DateTimeFormat.#formatFraction(fraction, DateTimeFormat.FIXED_NUMBER_FORMATS[tokenLen - 1]);
} else {
// 'F' format - remove trailing zeros
let effectiveDigits = tokenLen;
while (effectiveDigits > 0 && fraction % 10 === 0) {
fraction = Math.floor(fraction / 10);
effectiveDigits--;
}
if (effectiveDigits > 0) {
result += DateTimeFormat.#formatFraction(fraction, DateTimeFormat.FIXED_NUMBER_FORMATS[effectiveDigits - 1]);
} else if (result.endsWith('.')) {
result = result.slice(0, -1);
}
}
} else {
throw new ArgumentError("Invalid format string");
}
break;
case 't':
tokenLen = DateTimeFormat.#parseRepeatPattern(format, i, ch);
if (tokenLen === 1) {
const designator = dateTime.hour < 12 ? dtfi.amDesignator : dtfi.pmDesignator;
if (designator.length >= 1) {
result += designator[0];
}
} else {
result += dateTime.hour < 12 ? dtfi.amDesignator : dtfi.pmDesignator;
}
break;
case 'd':
tokenLen = DateTimeFormat.#parseRepeatPattern(format, i, ch);
if (tokenLen <= 2) {
const day = dateTime.day;
result += DateTimeFormat.#formatDigits(day, tokenLen);
} else {
const dayOfWeek = dateTime.dayOfWeek;
result += DateTimeFormat.#formatDayOfWeek(dayOfWeek, tokenLen, dtfi);
}
isTimeOnly = false;
break;
case 'M':
tokenLen = DateTimeFormat.#parseRepeatPattern(format, i, ch);
const month = dateTime.month;
if (tokenLen <= 2) {
result += DateTimeFormat.#formatDigits(month, tokenLen);
} else {
if (dtfi.useGenitiveMonth) {
const useGenitive = DateTimeFormat.#isUseGenitiveForm(format, i, tokenLen, 'd');
result += dtfi.getMonthName(month, useGenitive ? 'genitive' : 'regular', tokenLen === 3);
} else {
result += DateTimeFormat.#formatMonth(month, tokenLen, dtfi);
}
}
isTimeOnly = false;
break;
case 'y':
const year = dateTime.year;
tokenLen = DateTimeFormat.#parseRepeatPattern(format, i, ch);
if (tokenLen <= 2) {
result += DateTimeFormat.#formatDigits(year % 100, tokenLen);
} else if (tokenLen <= 16) {
result += DateTimeFormat.#formatDigits(year, tokenLen);
} else {
result += year.toString().padStart(tokenLen, '0');
}
isTimeOnly = false;
break;
case 'z':
tokenLen = DateTimeFormat.#parseRepeatPattern(format, i, ch);
result += DateTimeFormat.#formatCustomizedTimeZone(dateTime, offset, tokenLen, isTimeOnly);
break;
case 'K':
tokenLen = 1;
result += DateTimeFormat.#formatCustomizedRoundtripTimeZone(dateTime, offset);
break;
case ':':
result += dtfi.timeSeparator;
tokenLen = 1;
break;
case '/':
result += dtfi.dateSeparator;
tokenLen = 1;
break;
case "'":
case '"':
const quoted = DateTimeFormat.#parseQuoteString(format, i);
result += quoted.text;
tokenLen = quoted.length;
break;
case '%':
const nextChar = DateTimeFormat.#parseNextChar(format, i);
if (nextChar >= 0 && nextChar !== '%'.charCodeAt(0)) {
const nextCharStr = String.fromCharCode(nextChar);
result += DateTimeFormat.#formatCustomized(dateTime, nextCharStr, dtfi, offset);
tokenLen = 2;
} else {
throw new ArgumentError("Invalid format string");
}
break;
case '\\':
const escapedChar = DateTimeFormat.#parseNextChar(format, i);
if (escapedChar >= 0) {
result += String.fromCharCode(escapedChar);
tokenLen = 2;
} else {
throw new ArgumentError("Invalid format string");
}
break;
default:
result += ch;
tokenLen = 1;
break;
}
i += tokenLen;
}
return result;
}
/**
* Expands standard format character to custom pattern.
*
* @param {string} format - Single character standard format
* @param {DateTimeFormatInfo} dtfi - Format info
* @returns {string} Expanded custom pattern
*/
static expandStandardFormatToCustomPattern(format, dtfi) {
switch (format) {
case 'd': return dtfi.shortDatePattern;
case 'D': return dtfi.longDatePattern;
case 'f': return dtfi.longDatePattern + " " + dtfi.shortTimePattern;
case 'F': return dtfi.fullDateTimePattern;
case 'g': return dtfi.generalShortTimePattern;
case 'G': return dtfi.generalLongTimePattern;
case 'm':
case 'M': return dtfi.monthDayPattern;
case 'o':
case 'O': return DateTimeFormat.ROUNDTRIP_FORMAT;
case 'r':
case 'R': return dtfi.rfc1123Pattern;
case 's': return dtfi.sortableDateTimePattern;
case 't': return dtfi.shortTimePattern;
case 'T': return dtfi.longTimePattern;
case 'u': return dtfi.universalSortableDateTimePattern;
case 'U': return dtfi.fullDateTimePattern;
case 'y':
case 'Y': return dtfi.yearMonthPattern;
default:
throw new ArgumentError("Invalid format string");
}
}
/**
* Formats DateTime with optional format and provider.
*
* @param {DateTime} dateTime - DateTime to format
* @param {string} [format] - Format string
* @param {string} [locale] - Locale string (e.g., 'en-US')
* @param {TimeSpan|number} [offset] - Timezone offset
* @returns {string} Formatted datetime string
*/
static format(dateTime, format = null, locale = null, offset = DateTimeFormat.NULL_OFFSET) {
if (!(dateTime instanceof DateTime)) {
throw new TypeError("First argument must be a DateTime instance");
}
const dtfi = DateTimeFormatInfo.getInstance(locale);
if (!format) {
if (offset === DateTimeFormat.NULL_OFFSET) {
// Default DateTime formatting
format = dtfi.generalLongTimePattern;
} else {
// Default DateTimeOffset formatting
format = dtfi.dateTimeOffsetPattern;
}
} else if (format.length === 1) {
// Single character - expand to pattern
if (format === 'U') {
// Universal time format requires UTC conversion
if (offset !== DateTimeFormat.NULL_OFFSET) {
throw new ArgumentError("Universal format not supported for DateTimeOffset");
}
dateTime = new DateTime(dateTime.ticks, DateTimeKind.UTC);
}
format = DateTimeFormat.expandStandardFormatToCustomPattern(format, dtfi);
}
return DateTimeFormat.#formatCustomized(dateTime, format, dtfi, offset);
}
/**
* Checks if DateTime represents time-only for special case formatting.
*
* @private
* @param {DateTime} dateTime - DateTime to check
* @param {DateTimeFormatInfo} dtfi - Format info
* @returns {boolean} Whether this should be treated as time-only
*/
static #isTimeOnlySpecialCase(dateTime, dtfi) {
// Special case for certain calendars when time is less than 1 day
return dateTime.ticks < TimeSpan.TICKS_PER_DAY &&
dtfi.calendar && dtfi.calendar.isNonGregorian;
}
/**
* Formats DateTime with 'O' roundtrip format.
*
* @param {DateTime} dateTime - DateTime to format
* @param {TimeSpan|number} offset - Timezone offset
* @returns {string} Formatted string
*/
static formatO(dateTime, offset = DateTimeFormat.NULL_OFFSET) {
let kind = DateTimeKind.LOCAL;
let offsetSpan = null;
if (offset === DateTimeFormat.NULL_OFFSET) {
kind = dateTime.kind;
if (kind === DateTimeKind.LOCAL) {
const jsDate = dateTime.toDate();
offsetSpan = TimeSpan.fromMinutes(-jsDate.getTimezoneOffset());
}
} else {
offsetSpan = offset instanceof TimeSpan ? offset : TimeSpan.fromTicks(offset);
}
const year = dateTime.year;
const month = dateTime.month;
const day = dateTime.day;
const hour = dateTime.hour;
const minute = dateTime.minute;
const second = dateTime.second;
const fraction = Number(dateTime.ticks % BigInt(TimeSpan.TICKS_PER_SECOND));
let result = `${year.toString().padStart(4, '0')}-`;
result += `${month.toString().padStart(2, '0')}-`;
result += `${day.toString().padStart(2, '0')}T`;
result += `${hour.toString().padStart(2, '0')}:`;
result += `${minute.toString().padStart(2, '0')}:`;
result += `${second.toString().padStart(2, '0')}`;
if (fraction > 0) {
result += `.${fraction.toString().padStart(7, '0')}`;
} else {
result += `.0000000`;
}
if (kind === DateTimeKind.UTC || (offsetSpan && offsetSpan.ticks === 0)) {
result += 'Z';
} else if (offsetSpan) {
const totalMinutes = offsetSpan.totalMinutes;
const sign = totalMinutes >= 0 ? '+' : '-';
const absMinutes = Math.abs(totalMinutes);
const offsetHours = Math.floor(absMinutes / 60);
const offsetMins = absMinutes % 60;
result += `${sign}${offsetHours.toString().padStart(2, '0')}:${offsetMins.toString().padStart(2, '0')}`;
}
return result;
}
/**
* Formats DateTime with 'R' RFC1123 format.
*
* @param {DateTime} dateTime - DateTime to format
* @param {TimeSpan|number} offset - Timezone offset
* @returns {string} Formatted string
*/
static formatR(dateTime, offset = DateTimeFormat.NULL_OFFSET) {
// Convert to UTC for RFC1123
let utcDateTime = dateTime;
if (offset !== DateTimeFormat.NULL_OFFSET) {
const offsetSpan = offset instanceof TimeSpan ? offset : TimeSpan.fromTicks(offset);
utcDateTime = dateTime.subtract(offsetSpan);
} else if (dateTime.kind === DateTimeKind.LOCAL) {
const jsDate = dateTime.toDate();
const localOffset = TimeSpan.fromMinutes(-jsDate.getTimezoneOffset());
utcDateTime = dateTime.subtract(localOffset);
}
const dayName = DateTimeFormat.#invariantAbbreviatedDayNames[utcDateTime.dayOfWeek];
const monthName = DateTimeFormat.#invariantAbbreviatedMonthNames[utcDateTime.month - 1];
return `${dayName}, ${utcDateTime.day.toString().padStart(2, '0')} ${monthName} ${utcDateTime.year} ` +
`${utcDateTime.hour.toString().padStart(2, '0')}:${utcDateTime.minute.toString().padStart(2, '0')}:${utcDateTime.second.toString().padStart(2, '0')} GMT`;
}
/**
* Formats DateTime with 'S' sortable format.
*
* @param {DateTime} dateTime - DateTime to format
* @returns {string} Formatted string
*/
static formatS(dateTime) {
const year = dateTime.year;
const month = dateTime.month;
const day = dateTime.day;
const hour = dateTime.hour;
const minute = dateTime.minute;
const second = dateTime.second;
return `${year.toString().padStart(4, '0')}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}T` +
`${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}:${second.toString().padStart(2, '0')}`;
}
/**
* Formats DateTime with 'U' universal sortable format.
*
* @param {DateTime} dateTime - DateTime to format
* @param {TimeSpan|number} offset - Timezone offset
* @returns {string} Formatted string
*/
static formatU(dateTime, offset = DateTimeFormat.NULL_OFFSET) {
// Convert to UTC
let utcDateTime = dateTime;
if (offset !== DateTimeFormat.NULL_OFFSET) {
const offsetSpan = offset instanceof TimeSpan ? offset : TimeSpan.fromTicks(offset);
utcDateTime = dateTime.subtract(offsetSpan);
}
const year = utcDateTime.year;
const month = utcDateTime.month;
const day = utcDateTime.day;
const hour = utcDateTime.hour;
const minute = utcDateTime.minute;
const second = utcDateTime.second;
return `${year.toString().padStart(4, '0')}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')} ` +
`${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}:${second.toString().padStart(2, '0')}Z`;
}
/**
* Formats DateTime with invariant 'G' format (for performance).
*
* @param {DateTime} dateTime - DateTime to format
* @param {TimeSpan|number} offset - Timezone offset
* @returns {string} Formatted string
*/
static formatInvariantG(dateTime, offset = DateTimeFormat.NULL_OFFSET) {
const month = dateTime.month;
const day = dateTime.day;
const year = dateTime.year;
const hour = dateTime.hour;
const minute = dateTime.minute;
const second = dateTime.second;
let result = `${month.toString().padStart(2, '0')}/${day.toString().padStart(2, '0')}/${year.toString().padStart(4, '0')} ` +
`${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}:${second.toString().padStart(2, '0')}`;
if (offset !== DateTimeFormat.NULL_OFFSET) {
const offsetSpan = offset instanceof TimeSpan ? offset : TimeSpan.fromTicks(offset);
const totalMinutes = offsetSpan.totalMinutes;
const sign = totalMinutes >= 0 ? '+' : '-';
const absMinutes = Math.abs(totalMinutes);
const offsetHours = Math.floor(absMinutes / 60);
const offsetMins = absMinutes % 60;
result += ` ${sign}${offsetHours.toString().padStart(2, '0')}:${offsetMins.toString().padStart(2, '0')}`;
}
return result;
}
}