// (C) Copyright 2015 Moodle Pty Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { Injectable } from '@angular/core'; import moment, { LongDateFormatKey } from 'moment'; import { CoreConstants } from '@/core/constants'; import { makeSingleton, Translate } from '@singletons'; /* * "Utils" service with helper functions for date and time. */ @Injectable({ providedIn: 'root' }) export class CoreTimeUtilsProvider { protected static readonly FORMAT_REPLACEMENTS = { // To convert PHP strf format to Moment format. '%a': 'ddd', '%A': 'dddd', '%d': 'DD', '%e': 'D', // Not exactly the same. PHP adds a space instead of leading zero, Moment doesn't. '%j': 'DDDD', '%u': 'E', '%w': 'e', // It might not behave exactly like PHP, the first day could be calculated differently. '%U': 'ww', // It might not behave exactly like PHP, the first week could be calculated differently. '%V': 'WW', '%W': 'ww', // It might not behave exactly like PHP, the first week could be calculated differently. '%b': 'MMM', '%B': 'MMMM', '%h': 'MMM', '%m': 'MM', '%C' : '', // Not supported by Moment. '%g': 'GG', '%G': 'GGGG', '%y': 'YY', '%Y': 'YYYY', '%H': 'HH', '%k': 'H', // Not exactly the same. PHP adds a space instead of leading zero, Moment doesn't. '%I': 'hh', '%l': 'h', // Not exactly the same. PHP adds a space instead of leading zero, Moment doesn't. '%M': 'mm', '%p': 'A', '%P': 'a', '%r': 'hh:mm:ss A', '%R': 'HH:mm', '%S': 'ss', '%T': 'HH:mm:ss', '%X': 'LTS', '%z': 'ZZ', '%Z': 'ZZ', // Not supported by Moment, it was deprecated. Use the same as %z. '%c': 'LLLL', '%D': 'MM/DD/YY', '%F': 'YYYY-MM-DD', '%s': 'X', '%x': 'L', '%n': '\n', '%t': '\t', '%%': '%', }; /** * Convert a PHP format to a Moment format. * * @param format PHP format. * @return Converted format. */ convertPHPToMoment(format: string): string { if (typeof format != 'string') { // Not valid. return ''; } let converted = ''; let escaping = false; for (let i = 0; i < format.length; i++) { let char = format[i]; if (char == '%') { // It's a PHP format, try to convert it. i++; char += format[i] || ''; if (escaping) { // We were escaping some characters, stop doing it now. escaping = false; converted += ']'; } converted += typeof CoreTimeUtilsProvider.FORMAT_REPLACEMENTS[char] != 'undefined' ? CoreTimeUtilsProvider.FORMAT_REPLACEMENTS[char] : char; } else { // Not a PHP format. We need to escape them, otherwise the letters could be confused with Moment formats. if (!escaping) { escaping = true; converted += '['; } converted += char; } } if (escaping) { // Finish escaping. converted += ']'; } return converted; } /** * Fix format to use in an ion-datetime. * * @param format Format to use. * @return Fixed format. */ fixFormatForDatetime(format: string): string { if (!format) { return ''; } // The component ion-datetime doesn't support escaping characters ([]), so we remove them. let fixed = format.replace(/[[\]]/g, ''); if (fixed.indexOf('A') != -1) { // Do not use am/pm format because there is a bug in ion-datetime. fixed = fixed.replace(/ ?A/g, ''); fixed = fixed.replace(/h/g, 'H'); } return fixed; } /** * Returns hours, minutes and seconds in a human readable format * * @param seconds A number of seconds * @return Seconds in a human readable format. */ formatTime(seconds: number): string { const totalSecs = Math.abs(seconds); const years = Math.floor(totalSecs / CoreConstants.SECONDS_YEAR); let remainder = totalSecs - (years * CoreConstants.SECONDS_YEAR); const days = Math.floor(remainder / CoreConstants.SECONDS_DAY); remainder = totalSecs - (days * CoreConstants.SECONDS_DAY); const hours = Math.floor(remainder / CoreConstants.SECONDS_HOUR); remainder = remainder - (hours * CoreConstants.SECONDS_HOUR); const mins = Math.floor(remainder / CoreConstants.SECONDS_MINUTE); const secs = remainder - (mins * CoreConstants.SECONDS_MINUTE); const ss = Translate.instance.instant('core.' + (secs == 1 ? 'sec' : 'secs')); const sm = Translate.instance.instant('core.' + (mins == 1 ? 'min' : 'mins')); const sh = Translate.instance.instant('core.' + (hours == 1 ? 'hour' : 'hours')); const sd = Translate.instance.instant('core.' + (days == 1 ? 'day' : 'days')); const sy = Translate.instance.instant('core.' + (years == 1 ? 'year' : 'years')); let oyears = ''; let odays = ''; let ohours = ''; let omins = ''; let osecs = ''; if (years) { oyears = years + ' ' + sy; } if (days) { odays = days + ' ' + sd; } if (hours) { ohours = hours + ' ' + sh; } if (mins) { omins = mins + ' ' + sm; } if (secs) { osecs = secs + ' ' + ss; } if (years) { return oyears + ' ' + odays; } if (days) { return odays + ' ' + ohours; } if (hours) { return ohours + ' ' + omins; } if (mins) { return omins + ' ' + osecs; } if (secs) { return osecs; } return Translate.instance.instant('core.now'); } /** * Returns hours, minutes and seconds in a human readable format. * * @param duration Duration in seconds * @param precision Number of elements to have in precision. 0 or undefined to full precission. * @return Duration in a human readable format. */ formatDuration(duration: number, precision?: number): string { precision = precision || 5; const eventDuration = moment.duration(duration, 'seconds'); let durationString = ''; if (precision && eventDuration.years() > 0) { durationString += ' ' + moment.duration(eventDuration.years(), 'years').humanize(); precision--; } if (precision && eventDuration.months() > 0) { durationString += ' ' + moment.duration(eventDuration.months(), 'months').humanize(); precision--; } if (precision && eventDuration.days() > 0) { durationString += ' ' + moment.duration(eventDuration.days(), 'days').humanize(); precision--; } if (precision && eventDuration.hours() > 0) { durationString += ' ' + moment.duration(eventDuration.hours(), 'hours').humanize(); precision--; } if (precision && eventDuration.minutes() > 0) { durationString += ' ' + moment.duration(eventDuration.minutes(), 'minutes').humanize(); precision--; } return durationString.trim(); } /** * Returns duration in a short human readable format: minutes and seconds, in fromat: 3' 27''. * * @param duration Duration in seconds * @return Duration in a short human readable format. */ formatDurationShort(duration: number): string { const minutes = Math.floor(duration / 60); const seconds = duration - minutes * 60; const durations = []; if (minutes > 0) { durations.push(minutes + '\''); } if (seconds > 0 || minutes === 0) { durations.push(seconds + '\'\''); } return durations.join(' '); } /** * Return the current timestamp in a "readable" format: YYYYMMDDHHmmSS. * * @return The readable timestamp. */ readableTimestamp(): string { return moment(Date.now()).format('YYYYMMDDHHmmSS'); } /** * Return the current timestamp (UNIX format, seconds). * * @return The current timestamp in seconds. */ timestamp(): number { return Math.round(Date.now() / 1000); } /** * Convert a timestamp into a readable date. * * @param timestamp Timestamp in milliseconds. * @param format The format to use (lang key). Defaults to core.strftimedaydatetime. * @param convert If true (default), convert the format from PHP to Moment. Set it to false for Moment formats. * @param fixDay If true (default) then the leading zero from %d is removed. * @param fixHour If true (default) then the leading zero from %I is removed. * @return Readable date. */ userDate(timestamp: number, format?: string, convert: boolean = true, fixDay: boolean = true, fixHour: boolean = true): string { format = Translate.instance.instant(format ? format : 'core.strftimedaydatetime'); if (fixDay) { format = format!.replace(/%d/g, '%e'); } if (fixHour) { format = format!.replace('%I', '%l'); } // Format could be in PHP format, convert it to moment. if (convert) { format = this.convertPHPToMoment(format!); } return moment(timestamp).format(format); } /** * Convert a timestamp to the format to set to a datetime input. * * @param timestamp Timestamp to convert (in ms). If not provided, current time. * @return Formatted time. */ toDatetimeFormat(timestamp?: number): string { timestamp = timestamp || Date.now(); return this.userDate(timestamp, 'YYYY-MM-DDTHH:mm:ss.SSS', false) + 'Z'; } /** * Convert a text into user timezone timestamp. * * @param date To convert to timestamp. * @return Converted timestamp. */ convertToTimestamp(date: string): number { if (typeof date == 'string' && date.slice(-1) == 'Z') { return moment(date).unix() - (moment().utcOffset() * 60); } return moment(date).unix(); } /** * Return the localized ISO format (i.e DDMMYY) from the localized moment format. Useful for translations. * DO NOT USE this function for ion-datetime format. Moment escapes characters with [], but ion-datetime doesn't support it. * * @param localizedFormat Format to use. * @return Localized ISO format */ getLocalizedDateFormat(localizedFormat: LongDateFormatKey): string { return moment.localeData().longDateFormat(localizedFormat); } /** * For a given timestamp get the midnight value in the user's timezone. * * The calculation is performed relative to the user's midnight timestamp * for today to ensure that timezones are preserved. * * @param timestamp The timestamp to calculate from. If not defined, return today's midnight. * @return The midnight value of the user's timestamp. */ getMidnightForTimestamp(timestamp?: number): number { if (timestamp) { return moment(timestamp * 1000).startOf('day').unix(); } else { return moment().startOf('day').unix(); } } } export class CoreTimeUtils extends makeSingleton(CoreTimeUtilsProvider) {}