extract date-time-moment-parser

refs FOO-1891

this is the start of a series to break apart the inter-dependencies
between timezone and moment.js

the formatting that is done by moment now stands alone in
packages/date-time-moment-parser and is used by ui/shared/timezone. The
formatter depends on phrases coming from the locale files, which are
available to ui/* but not to packages/*, so in an initializer we inject
our custom formats into the package, allowing it to make use of them.

there should be no breaking API changes, even though several APIs were
dropped, as they were all used in test but not in the app code. To
minimize the changes, a specHelpers.js file is now provided by the
timezone package that maintains backwards compat to a reasonable extent.

TEST PLAN
==== ====

although the tests should be covering this, it wouldn't hurt to manually
exercise any of the date picker widgets

Change-Id: I0c59ad2df8f7392425debb6ec448ec1b4fb029c6
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/265313
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Charley Kline <ckline@instructure.com>
Product-Review: Charley Kline <ckline@instructure.com>
QA-Review: Charley Kline <ckline@instructure.com>
This commit is contained in:
Ahmad Amireh 2021-04-07 11:58:44 -06:00
parent 1b0b4b035d
commit 83e4f6d4ab
50 changed files with 2861 additions and 1548 deletions

View File

@ -33,7 +33,7 @@ module.exports = {
// mock the tinymce-react Editor react component
'@tinymce/tinymce-react': '<rootDir>/packages/canvas-rce/src/rce/__mocks__/tinymceReact.js'
},
roots: ['ui', 'gems/plugins', 'public/javascripts'],
roots: ['<rootDir>/ui', 'gems/plugins', 'public/javascripts'],
moduleDirectories: ['ui/shims', 'public/javascripts', 'node_modules'],
reporters: [
'default',

View File

@ -19,6 +19,9 @@
import Enzyme from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'
import {filterUselessConsoleMessages} from '@instructure/js-utils'
import {
up as configureDateTimeMomentParser
} from '../ui/boot/initializers/configureDateTimeMomentParser'
filterUselessConsoleMessages(console)
@ -34,6 +37,8 @@ Enzyme.configure({adapter: new Adapter()})
// because InstUI themeable components need an explicit "dir" attribute on the <html> element
document.documentElement.setAttribute('dir', 'ltr')
configureDateTimeMomentParser()
// because everyone implements `flat()` and `flatMap()` except JSDOM 🤦🏼‍♂️
if (!Array.prototype.flat) {
// eslint-disable-next-line no-extend-native

View File

@ -0,0 +1,106 @@
/*
* Copyright (C) 2021 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import moment from 'moment'
export default function getFormats({ customI18nFormats }) {
const i18nFormatsForCurrentLocale = customI18nFormats.map(x => x()).filter(x => !!x)
const momentCompatibleI18nFormats = specifyMinutesImplicitly(
i18nFormatsForCurrentLocale
).map(convertI18nFormatToMomentFormat)
return union(momentStockFormats, momentCompatibleI18nFormats)
}
const union = (a, b) => b.reduce((acc, x) => {
if (!acc.includes(x)) {
acc.push(x)
}
return acc
}, [].concat(a))
const i18nToMomentTokenMapping = {
'%A': 'dddd',
'%B': 'MMMM',
'%H': 'HH',
'%M': 'mm',
'%S': 'ss',
'%P': 'a',
'%Y': 'YYYY',
'%a': 'ddd',
'%b': 'MMM',
'%m': 'M',
'%d': 'D',
'%k': 'H',
'%l': 'h',
'%z': 'Z',
'%-H': 'H',
'%-M': 'm',
'%-S': 's',
'%-m': 'M',
'%-d': 'D',
'%-k': 'H',
'%-l': 'h'
}
const momentStockFormats = [
moment.ISO_8601,
'YYYY',
'LT',
'LTS',
'L',
'l',
'LL',
'll',
'LLL',
'lll',
'LLLL',
'llll',
'D MMM YYYY',
'H:mm'
]
// expand every i18n format that specifies minutes (%M or %-M) into two: one
// that specifies the minutes and another that doesn't
//
// why? don't ask me
const specifyMinutesImplicitly = (formats) => formats.map(format =>
format.match(/:%-?M/) ?
[format, format.replace(/:%-?M/, '')] :
[format]
).flat(1)
const convertI18nFormatToMomentFormat = i18nFormat => {
const escapeNonI18nTokens = (string) => (
string.split(' ').map(escapeUnlessIsI18nToken).join(' ')
)
const escapeUnlessIsI18nToken = (string) => {
const isKey = Object.keys(i18nToMomentTokenMapping).find(k => string.indexOf(k) > -1)
return isKey ? string : `[${string}]`
}
const escapedI18nFormat = escapeNonI18nTokens(i18nFormat)
return Object.keys(i18nToMomentTokenMapping).reduce((acc, i18nToken) => (
acc.replace(i18nToken, i18nToMomentTokenMapping[i18nToken])
), escapedI18nFormat)
}

View File

@ -0,0 +1,86 @@
/*
* Copyright (C) 2021 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import moment from 'moment'
import getFormats from './formats'
const config = { customI18nFormats: [] }
// Parse a DateTime string according to any of the pre-defined formats. The
// formats may come from Moment itself, or from the locale dictionary for
// date and time.
//
// See ./formats.js for the possible formats.
//
// @param {Object.<{
// t: (String) -> String,
// lookup: (String) -> String
// }>}
// An object that can query the locale dictionary for datetime formats.
//
// @param {String}
// The string to parse.
//
// @return {Moment?}
// A moment instance in case the string could be parsed.
export default function parseDateTime(input, locale) {
const formats = getFormats(config)
const momentInstance = createDateTimeMoment(input, formats, locale)
return momentInstance.isValid() ? momentInstance : null
}
export function useI18nFormats(customI18nFormats) {
config.customI18nFormats = customI18nFormats
}
// Check a moment instance to see if its format (and input value) explicitly
// specify a timezone. This query is useful to know whether the date needs to
// be unfudged in case it does NOT specify a timezone (e.g. using tz-parse).
export function specifiesTimezone(m) {
return !!(m._f.match(/Z/) && m._pf.unusedTokens.indexOf('Z') === -1)
}
export function toRFC3339WithoutTZ(m) {
return moment(m).locale('en').format('YYYY-MM-DD[T]HH:mm:ss')
}
// wrap's moment() for parsing datetime strings. assumes the string to be
// parsed is in the profile timezone unless if contains an offset string
// *and* a format token to parse it, and unfudges the result.
export function createDateTimeMoment(input, format, locale) {
// ensure first argument is a string and second is a format or an array
// of formats
if (typeof input !== 'string' || !(typeof format === 'string' || Array.isArray(format))) {
throw new Error(
'createDateTimeMoment only works on string+format(s). just use moment() directly for any other signature'
)
}
let m = moment.apply(null, [input, format, locale])
if (m._pf.unusedTokens.length > 0) {
// we didn't use strict at first, because we want to accept when
// there's unused input as long as we're using all tokens. but if the
// best non-strict match has unused tokens, reparse with strict
return moment.apply(null, [input, format, locale, true])
}
else {
return m
}
}

View File

@ -0,0 +1,18 @@
{
"name": "date-time-moment-parser",
"version": "1.0.0",
"description": "parse DateTime strings into Date objects using moment.js",
"main": "index.js",
"type": "module",
"jest": {
"transform": {}
},
"scripts": {
"test": "NODE_OPTIONS=--experimental-vm-modules jest"
},
"author": "Ahmad Amireh <ahmad@instructure.com>",
"license": "MIT",
"devDependencies": {
"jest": "^26.6.3"
}
}

View File

@ -0,0 +1,139 @@
/*
* Copyright (C) 2021 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import parseDateTime, { createDateTimeMoment, specifiesTimezone, toRFC3339WithoutTZ } from './index'
import tz from 'timezone'
import moment from 'moment'
import detroit from 'timezone/America/Detroit'
const moonwalk = new Date(Date.UTC(1969, 6, 21, 2, 56))
const tzDetroit = tz(detroit, 'America/Detroit')
const I18nStub = {
lookup: () => null,
t: () => null
}
let originalMomentLocale
beforeEach(() => {
originalMomentLocale = moment.locale()
})
afterEach(() => {
moment.locale(originalMomentLocale)
originalMomentLocale = null
})
test('moment(one-arg) complains', () => {
expect(() => {
createDateTimeMoment('June 24 at 10:00pm')
}).toThrowError(/ only works on /)
})
test('moment(non-string, fmt-string) complains', () => {
expect(() => {
createDateTimeMoment(moonwalk, 'MMMM D h:mmA')
}).toThrowError(/ only works on /)
})
test('moment(date-string, non-string) complains', () => {
expect(() => {
createDateTimeMoment('June 24 at 10:00pm', 123)
}).toThrowError(/ only works on /)
})
test('moment(date-string, fmt-string) works', () =>
expect(createDateTimeMoment('June 24 at 10:00pm', 'MMMM D h:mmA')).toBeTruthy()
)
test('moment(date-string, [fmt-strings]) works', () =>
expect(createDateTimeMoment('June 24 at 10:00pm', ['MMMM D h:mmA', 'L'])).toBeTruthy()
)
test('moment passes through invalid results', () => {
const m = createDateTimeMoment('not a valid date', 'L')
expect(m.isValid()).toEqual(false)
})
test('moment accepts excess input, but all format used', () => {
const m = createDateTimeMoment('12pm and more', 'ha')
expect(m.isValid()).toEqual(true)
})
test('moment rejects excess format', () => {
const m = createDateTimeMoment('12pm', 'h:mma')
expect(m.isValid()).toEqual(false)
})
test('moment returns moment for valid results', () => {
const m = createDateTimeMoment('June 24, 2015 at 10:00pm -04:00', 'MMMM D, YYYY h:mmA Z')
expect(m.isValid()).toEqual(true)
})
test('moment sans-timezone info parses according to profile timezone', () => {
const expected = new Date(1435197600000) // 10pm EDT on June 24, 2015
const m = createDateTimeMoment('June 24, 2015 at 10:00pm', 'MMMM D, YYYY h:mmA')
expect(specifiesTimezone(m)).toEqual(false)
expect(tzDetroit(toRFC3339WithoutTZ(m))).toEqual(+expected)
})
test('moment with-timezone info parses according to that timezone', () => {
const expected = new Date(1435204800000) // 10pm MDT on June 24, 2015
const m = createDateTimeMoment('June 24, 2015 at 10:00pm -06:00', 'MMMM D, YYYY h:mmA Z')
expect(specifiesTimezone(m)).toEqual(true)
expect(+m.toDate()).toEqual(+expected)
expect(tzDetroit(m.format())).toEqual(+expected)
})
test('moment can change locales with single arity', () => {
moment.locale('en')
const m1 = createDateTimeMoment('mercredi 1 juillet 2015 15:00', 'LLLL')
expect(m1._locale._abbr.match(/fr/)).toBeFalsy()
expect(m1.isValid()).toBeFalsy()
moment.locale('fr')
const m2 = createDateTimeMoment('mercredi 1 juillet 2015 15:00', 'LLLL')
expect(m2._locale._abbr.match(/fr/)).toBeTruthy()
expect(m2.isValid()).toBeTruthy()
})
describe('specifiesTimezone', () => {
it('is true when format contains Z and input contains a timezone', () => {
expect(
specifiesTimezone(
createDateTimeMoment('June 24, 2015 at 10:00pm -06:00', 'MMMM D, YYYY h:mmA Z')
)
).toEqual(true)
})
it('is false when format contains Z but input has no timezone', () => {
expect(
specifiesTimezone(
createDateTimeMoment('June 24, 2015 at 10:00pm', 'MMMM D, YYYY h:mmA Z')
)
).toEqual(false)
})
it('is false when format does not contain Z even if the input contains a timezone', () => {
expect(
specifiesTimezone(
createDateTimeMoment('June 24, 2015 at 10:00pm -06:00', 'MMMM D, YYYY h:mmA')
)
).toEqual(false)
})
})

View File

@ -18,7 +18,9 @@
import $ from 'jquery'
import {isArray, isObject, uniq} from 'lodash'
import tz from '@canvas/timezone'
import tzInTest from '@canvas/timezone/specHelpers'
import { configure as configureTimezone } from '@canvas/timezone'
import timezone from 'timezone'
import fcUtil from '@canvas/calendar/jquery/fcUtil.coffee'
import denver from 'timezone/America/Denver'
import juneau from 'timezone/America/Juneau'
@ -67,16 +69,21 @@ QUnit.module('AgendaView', {
fcUtil.addMinuteDelta(this.startDate, -60 * 24 * 15)
this.dataSource = new EventDataSource(this.contexts)
this.server = sinon.fakeServer.create()
this.snapshot = tz.snapshot()
tz.changeZone(denver, 'America/Denver')
tzInTest.configureAndRestoreLater({
tz: timezone(denver, 'America/Denver'),
tzData: {
'America/Denver': denver
},
momentLocale: 'en'
})
I18nStubber.pushFrame()
fakeENV.setup({CALENDAR: {}})
},
teardown() {
this.container.remove()
this.server.restore()
tz.restore(this.snapshot)
I18nStubber.popFrame()
tzInTest.restore()
I18nStubber.clear()
fakeENV.teardown()
}
})
@ -188,7 +195,14 @@ test('should only include days on page breaks once', function() {
})
test('renders non-assignment events with locale-appropriate format string', function() {
tz.changeLocale(french, 'fr_FR', 'fr')
tzInTest.configureAndRestoreLater({
tz: timezone(denver, 'America/Denver', french, 'fr_FR'),
tzData: {
'America/Denver': denver,
},
momentLocale: 'fr'
})
// tz.changeLocale(french, 'fr_FR', 'fr')
I18nStubber.setLocale('fr_FR')
I18nStubber.stub('fr_FR', {'time.formats.tiny': '%k:%M'})
const view = new AgendaView({
@ -210,7 +224,13 @@ test('renders non-assignment events with locale-appropriate format string', func
})
test('renders assignment events with locale-appropriate format string', function() {
tz.changeLocale(french, 'fr_FR', 'fr')
tzInTest.configureAndRestoreLater({
tz: timezone(denver, 'America/Denver', french, 'fr_FR'),
tzData: {
'America/Denver': denver,
},
momentLocale: 'fr'
})
I18nStubber.setLocale('fr_FR')
I18nStubber.stub('fr_FR', {'time.formats.tiny': '%k:%M'})
const view = new AgendaView({
@ -232,7 +252,13 @@ test('renders assignment events with locale-appropriate format string', function
})
test('renders non-assignment events in appropriate timezone', function() {
tz.changeZone(juneau, 'America/Juneau')
tzInTest.configureAndRestoreLater({
tz: timezone(juneau, 'America/Juneau'),
tzData: {
'America/Juneau': juneau
}
})
I18nStubber.stub('en', {
'time.formats.tiny': '%l:%M%P',
date: {}
@ -256,7 +282,12 @@ test('renders non-assignment events in appropriate timezone', function() {
})
test('renders assignment events in appropriate timezone', function() {
tz.changeZone(juneau, 'America/Juneau')
tzInTest.configureAndRestoreLater({
tz: timezone(juneau, 'America/Juneau'),
tzData: {
'America/Juneau': juneau
}
})
I18nStubber.stub('en', {
'time.formats.tiny': '%l:%M%P',
date: {}

View File

@ -22,6 +22,8 @@ import I18n from 'i18n!calendar'
import fcUtil from '@canvas/calendar/jquery/fcUtil.coffee'
import moment from 'moment'
import tz from '@canvas/timezone'
import tzInTest from '@canvas/timezone/specHelpers'
import timezone from 'timezone'
import denver from 'timezone/America/Denver'
import fixtures from 'helpers/fixtures'
import $ from 'jquery'
@ -30,14 +32,19 @@ import fakeENV from 'helpers/fakeENV'
QUnit.module('Calendar', {
setup() {
this.snapshot = tz.snapshot()
tz.changeZone(denver, 'America/Denver')
tzInTest.configureAndRestoreLater({
tz: timezone(denver, 'America/Denver'),
tzData: {
'America/Denver': denver
}
})
fixtures.setup()
sinon.stub($, 'getJSON')
fakeENV.setup()
},
teardown() {
tz.restore(this.snapshot)
tzInTest.restore()
const calendar = $('#fixtures .calendar').data('fullCalendar')
if (calendar) {
calendar.destroy()

View File

@ -71,6 +71,7 @@ QUnit.module('EditAssignmentDetails', {
document.getElementById('fixtures').innerHTML = ''
fakeENV.teardown()
tz.restore(this.snapshot)
I18nStubber.clear()
}
})
const createView = function(model, event) {
@ -135,7 +136,6 @@ test('should localize start date', function() {
})
const view = createView(commonEvent(), this.event)
equal(view.$('.datetime_field').val(), 'ven. 7 août 2015 17:00')
I18nStubber.popFrame()
})
test('requires name to save assignment event', function() {

View File

@ -49,6 +49,7 @@ QUnit.module('EditAssignmentDetails', {
document.getElementById('fixtures').innerHTML = ''
fakeENV.teardown()
tz.restore(this.snapshot)
I18nStubber.clear()
}
})
const createView = function(event = note) {
@ -74,7 +75,6 @@ test('should localize start date', () => {
})
const view = createView(commonEvent())
equal(view.$('.date_field').val(), '22 juil. 2017')
I18nStubber.popFrame()
})
test('requires name to save assignment note', () => {

View File

@ -18,15 +18,21 @@
import fcUtil from '@canvas/calendar/jquery/fcUtil.coffee'
import tz from '@canvas/timezone'
import tzInTest from '@canvas/timezone/specHelpers'
import timezone from 'timezone'
import denver from 'timezone/America/Denver'
QUnit.module('Calendar', {
setup() {
this.snapshot = tz.snapshot()
tz.changeZone(denver, 'America/Denver')
tzInTest.configureAndRestoreLater({
tz: timezone(denver, 'America/Denver'),
tzData: {
'America/Denver': denver
}
})
},
teardown() {
tz.restore(this.snapshot)
tzInTest.restore()
}
})

View File

@ -23,6 +23,8 @@ import assertions from 'helpers/assertions'
import fakeENV from 'helpers/fakeENV'
import numberFormat from '@canvas/i18n/numberFormat'
import tz from '@canvas/timezone'
import tzInTest from '@canvas/timezone/specHelpers'
import timezone from 'timezone'
import detroit from 'timezone/America/Detroit'
import chicago from 'timezone/America/Chicago'
import newYork from 'timezone/America/New_York'
@ -125,12 +127,11 @@ test('supports truncation left', () => {
QUnit.module('friendlyDatetime', {
setup() {
this.snapshot = tz.snapshot()
return tz.changeZone(detroit, 'America/Detroit')
},
teardown() {
tz.restore(this.snapshot)
tzInTest.restore()
}
})
@ -166,16 +167,20 @@ test('includes a visible version', () =>
QUnit.module('contextSensitive FriendlyDatetime', {
setup() {
this.snapshot = tz.snapshot()
fakeENV.setup()
ENV.CONTEXT_TIMEZONE = 'America/Chicago'
tz.changeZone(detroit, 'America/Detroit')
return tz.preload('America/Chicago', chicago)
tzInTest.configureAndRestoreLater({
tz: timezone(detroit, 'America/Detroit'),
tzData: {
'America/Chicago': chicago,
'America/Detroit': detroit,
}
})
},
teardown() {
fakeENV.teardown()
tz.restore(this.snapshot)
tzInTest.restore()
}
})
@ -216,14 +221,19 @@ QUnit.module('contextSensitiveDatetimeTitle', {
this.snapshot = tz.snapshot()
fakeENV.setup()
ENV.CONTEXT_TIMEZONE = 'America/Chicago'
tz.changeZone(detroit, 'America/Detroit')
tz.preload('America/Chicago', chicago)
return tz.preload('America/New_York', newYork)
tzInTest.configureAndRestoreLater({
tz: timezone(detroit, 'America/Detroit'),
tzData: {
'America/Chicago': chicago,
'America/Detroit': detroit,
'America/New_York': newYork
}
})
},
teardown() {
fakeENV.teardown()
tz.restore(this.snapshot)
tzInTest.restore()
}
})
@ -244,7 +254,13 @@ test('splits title text to both zones', () => {
test('properly spans day boundaries', () => {
ENV.TIMEZONE = 'America/Chicago'
tz.changeZone(chicago, 'America/Chicago')
tzInTest.configureAndRestoreLater({
tz: timezone(chicago, 'America/Chicago'),
tzData: {
'America/Chicago': chicago,
'America/New_York': newYork
}
})
ENV.CONTEXT_TIMEZONE = 'America/New_York'
const titleText = helpers.contextSensitiveDatetimeTitle('1970-01-01 05:30:00Z', {
hash: {justText: true}

View File

@ -17,8 +17,10 @@
*/
import I18n from '@canvas/i18n'
import 'translations/_core_en'
const frames = []
const enTranslations = I18n.translations.en
export default {
pushFrame() {
@ -40,6 +42,11 @@ export default {
clear() {
while (frames.length > 0) this.popFrame()
},
useInitialTranslations() {
this.pushFrame()
I18n.locale = 'en'
I18n.translations = { en: enTranslations }
},
stub(locale, translations, cb) {
if (cb) {
return this.withFrame(() => this.stub(locale, translations), cb)

View File

@ -34,7 +34,7 @@ QUnit.module('I18n', {
},
teardown() {
return I18nStubber.popFrame()
return I18nStubber.clear()
}
})
@ -159,7 +159,7 @@ QUnit.module('I18n localize number', {
},
teardown() {
return I18nStubber.popFrame()
return I18nStubber.clear()
}
})

View File

@ -18,6 +18,8 @@
import $ from 'jquery'
import tz from '@canvas/timezone'
import tzInTest from '@canvas/timezone/specHelpers'
import timezone from 'timezone'
import detroit from 'timezone/America/Detroit'
import juneau from 'timezone/America/Juneau'
import kolkata from 'timezone/Asia/Kolkata'
@ -27,11 +29,10 @@ import '@canvas/datetime'
QUnit.module('fudgeDateForProfileTimezone', {
setup() {
this.snapshot = tz.snapshot()
this.original = new Date(Date.UTC(2013, 8, 1))
},
teardown() {
tz.restore(this.snapshot)
tzInTest.restore()
}
})
@ -42,7 +43,17 @@ test('should produce a date that formats via toString same as the original forma
test('should parse dates before the year 1000', () => {
// using specific string (and specific timezone to guarantee it) since tz.format has a bug pre-1000
tz.changeZone(detroit, 'America Detroit')
//
// TODO: in 2021, this appears to be bogus as it's never actually specifying
// the timezone as the comment above states because "America Detroit" doesn't
// resolve to one (America/Detroit does) and tz just ends up using UTC
tzInTest.configureAndRestoreLater({
tz: timezone(detroit, 'America Detroit'),
tzData: {
'America Detroit': detroit,
}
})
const oldDate = new Date(Date.UTC(900, 1, 1, 0, 0, 0))
const oldFudgeDate = $.fudgeDateForProfileTimezone(oldDate)
equal(oldFudgeDate.toString('yyyy-MM-dd HH:mm:ss'), '0900-02-01 00:00:00')
@ -65,21 +76,30 @@ test('should not return treat 0 as invalid', () =>
equal(+$.fudgeDateForProfileTimezone(0), +$.fudgeDateForProfileTimezone(new Date(0))))
test('should be sensitive to profile time zone', function () {
tz.changeZone(detroit, 'America/Detroit')
tzInTest.configureAndRestoreLater({
tz: timezone(detroit, 'America/Detroit'),
tzData: {
'America/Detroit': detroit,
}
})
let fudged = $.fudgeDateForProfileTimezone(this.original)
equal(fudged.toString('yyyy-MM-dd HH:mm:ss'), tz.format(this.original, '%F %T'))
tz.changeZone(juneau, 'America/Juneau')
tzInTest.configureAndRestoreLater({
tz: timezone(juneau, 'America/Juneau'),
tzData: {
'America/Juneau': juneau,
}
})
fudged = $.fudgeDateForProfileTimezone(this.original)
equal(fudged.toString('yyyy-MM-dd HH:mm:ss'), tz.format(this.original, '%F %T'))
})
QUnit.module('unfudgeDateForProfileTimezone', {
setup() {
this.snapshot = tz.snapshot()
this.original = new Date(Date.UTC(2013, 8, 1))
},
teardown() {
tz.restore(this.snapshot)
tzInTest.restore()
}
})
@ -105,20 +125,28 @@ test('should not return treat 0 as invalid', () =>
equal(+$.unfudgeDateForProfileTimezone(0), +$.unfudgeDateForProfileTimezone(new Date(0))))
test('should be sensitive to profile time zone', function () {
tz.changeZone(detroit, 'America/Detroit')
tzInTest.configureAndRestoreLater({
tz: timezone(detroit, 'America/Detroit'),
tzData: {
'America/Detroit': detroit,
}
})
let unfudged = $.unfudgeDateForProfileTimezone(this.original)
equal(tz.format(unfudged, '%F %T'), this.original.toString('yyyy-MM-dd HH:mm:ss'))
tz.changeZone(juneau, 'America/Juneau')
tzInTest.configureAndRestoreLater({
tz: timezone(juneau, 'America/Juneau'),
tzData: {
'America/Juneau': juneau,
}
})
unfudged = $.unfudgeDateForProfileTimezone(this.original)
equal(tz.format(unfudged, '%F %T'), this.original.toString('yyyy-MM-dd HH:mm:ss'))
})
QUnit.module('sameYear', {
setup() {
this.snapshot = tz.snapshot()
},
teardown() {
tz.restore(this.snapshot)
tzInTest.restore()
}
})
@ -131,7 +159,12 @@ test('should return true iff both dates from same year', () => {
})
test('should compare relative to profile timezone', () => {
tz.changeZone(detroit, 'America/Detroit')
tzInTest.configureAndRestoreLater({
tz: timezone(detroit, 'America/Detroit'),
tzData: {
'America/Detroit': detroit,
}
})
const date1 = new Date(5 * 3600000) // 5am UTC = 12am EST
const date2 = new Date(+date1 + 1000) // Jan 1, 1970 at 11:59:59pm EST
const date3 = new Date(+date1 - 1000) // Jan 2, 1970 at 00:00:01am EST
@ -140,11 +173,8 @@ test('should compare relative to profile timezone', () => {
})
QUnit.module('sameDate', {
setup() {
this.snapshot = tz.snapshot()
},
teardown() {
tz.restore(this.snapshot)
tzInTest.restore()
}
})
@ -157,7 +187,12 @@ test('should return true iff both times from same day', () => {
})
test('should compare relative to profile timezone', () => {
tz.changeZone(detroit, 'America/Detroit')
tzInTest.configureAndRestoreLater({
tz: timezone(detroit, 'America/Detroit'),
tzData: {
'America/Detroit': detroit,
}
})
const date1 = new Date(86400000 + 5 * 3600000)
const date2 = new Date(+date1 + 1000)
const date3 = new Date(+date1 - 1000)
@ -167,35 +202,43 @@ test('should compare relative to profile timezone', () => {
QUnit.module('dateString', {
setup() {
this.snapshot = tz.snapshot()
I18nStubber.pushFrame()
},
teardown() {
tz.restore(this.snapshot)
I18nStubber.popFrame()
tzInTest.restore()
I18nStubber.clear()
}
})
test('should format in profile timezone', () => {
I18nStubber.stub('en', {'date.formats.medium': '%b %-d, %Y'})
tz.changeZone(detroit, 'America/Detroit')
tzInTest.configureAndRestoreLater({
tz: timezone(detroit, 'America/Detroit'),
tzData: {
'America/Detroit': detroit,
}
})
equal($.dateString(new Date(0)), 'Dec 31, 1969')
})
QUnit.module('timeString', {
setup() {
this.snapshot = tz.snapshot()
I18nStubber.pushFrame()
},
teardown() {
tz.restore(this.snapshot)
I18nStubber.popFrame()
tzInTest.restore()
I18nStubber.clear()
}
})
test('should format in profile timezone', () => {
I18nStubber.stub('en', {'time.formats.tiny': '%l:%M%P'})
tz.changeZone(detroit, 'America/Detroit')
tzInTest.configureAndRestoreLater({
tz: timezone(detroit, 'America/Detroit'),
tzData: {
'America/Detroit': detroit,
}
})
equal($.timeString(new Date(60000)), '7:01pm')
})
@ -207,7 +250,12 @@ test('should format according to profile locale', () => {
test('should use the tiny_on_the_hour format on the hour', () => {
I18nStubber.stub('en', {'time.formats.tiny_on_the_hour': '%l%P'})
tz.changeZone(detroit, 'America/Detroit')
tzInTest.configureAndRestoreLater({
tz: timezone(detroit, 'America/Detroit'),
tzData: {
'America/Detroit': detroit,
}
})
equal($.timeString(new Date(0)), '7pm')
})
@ -215,23 +263,33 @@ test('should use the tiny format on the hour, when timezone difference is not in
I18nStubber.stub('en', {'time.formats.tiny': '%l:%M%P'})
I18nStubber.stub('en', {'time.formats.tiny_on_the_hour': '%l%P'})
// kolkata: +05:30
tz.changeZone(kolkata, 'Asia/Kolkata')
tzInTest.configureAndRestoreLater({
tz: timezone(kolkata, 'Asia/Kolkata'),
tzData: {
'Asia/Kolkata': kolkata,
'America/Detroit': detroit
}
})
equal($.timeString(new Date(30 * 60 * 1000), {timezone: 'America/Detroit'}), '7:30pm')
})
QUnit.module('datetimeString', {
setup() {
this.snapshot = tz.snapshot()
I18nStubber.pushFrame()
},
teardown() {
tz.restore(this.snapshot)
I18nStubber.popFrame()
tzInTest.restore()
I18nStubber.clear()
}
})
test('should format in profile timezone', () => {
tz.changeZone(detroit, 'America/Detroit')
tzInTest.configureAndRestoreLater({
tz: timezone(detroit, 'America/Detroit'),
tzData: {
'America/Detroit': detroit,
}
})
I18nStubber.stub('en', {
'date.formats.medium': '%b %-d, %Y',
'time.formats.tiny': '%l:%M%P',
@ -241,7 +299,11 @@ test('should format in profile timezone', () => {
})
test('should translate into the profile locale', () => {
tz.changeLocale(portuguese, 'pt_PT', 'pt')
tzInTest.configureAndRestoreLater({
tz: timezone(portuguese, 'pt_PT'),
momentLocale: 'pt'
})
I18nStubber.setLocale('pt')
I18nStubber.stub('pt', {
'date.formats.medium': '%-d %b %Y',
@ -253,18 +315,23 @@ test('should translate into the profile locale', () => {
QUnit.module('$.datepicker.parseDate', {
setup() {
this.snapshot = tz.snapshot()
I18nStubber.pushFrame()
},
teardown() {
tz.restore(this.snapshot)
I18nStubber.popFrame()
tzInTest.restore()
I18nStubber.clear()
}
})
test('should accept localized strings and return them fudged', () => {
tz.changeZone(detroit, 'America/Detroit')
tz.changeLocale(portuguese, 'pt_PT', 'pt')
tzInTest.configureAndRestoreLater({
tz: timezone(detroit, 'America/Detroit', portuguese, 'pt_PT'),
tzData: {
'America/Detroit': detroit
},
momentLocale: 'pt'
})
I18nStubber.setLocale('pt')
I18nStubber.stub('pt', {
// this isn't the real format, but we want the %Y in here to make it

View File

@ -21,6 +21,7 @@ import {mount} from 'enzyme'
import chicago from 'timezone/America/Chicago'
import DueDateCalendarPicker from '@canvas/due-dates/react/DueDateCalendarPicker'
import tz from '@canvas/timezone'
import tzInTest from '@canvas/timezone/specHelpers'
import french from 'timezone/fr_FR'
import I18nStubber from 'helpers/I18nStubber'
import fakeENV from 'helpers/fakeENV'
@ -52,6 +53,7 @@ QUnit.module('DueDateCalendarPicker', suiteHooks => {
wrapper.unmount()
clock.restore()
fakeENV.teardown()
tzInTest.restore()
})
function mountComponent() {
@ -79,10 +81,8 @@ QUnit.module('DueDateCalendarPicker', suiteHooks => {
test('converts to fancy midnight in the timezone of the user', () => {
props.isFancyMidnight = true
mountComponent()
const snapshot = tz.snapshot()
tz.changeZone(chicago, 'America/Chicago')
tzInTest.changeZone(chicago, 'America/Chicago')
simulateChange('2015-08-31T00:00:00')
tz.restore(snapshot)
equal(getEnteredDate().toUTCString(), 'Tue, 01 Sep 2015 04:59:59 GMT')
})
@ -99,8 +99,7 @@ QUnit.module('DueDateCalendarPicker', suiteHooks => {
test('#formattedDate() returns a localized Date', () => {
mountComponent()
const snapshot = tz.snapshot()
tz.changeLocale(french, 'fr_FR', 'fr')
tzInTest.changeLocale(french, 'fr_FR', 'fr')
I18nStubber.pushFrame()
I18nStubber.setLocale('fr_FR')
I18nStubber.stub('fr_FR', {
@ -108,8 +107,7 @@ QUnit.module('DueDateCalendarPicker', suiteHooks => {
'time.formats.tiny': '%-k:%M'
})
equal(wrapper.instance().formattedDate(), '1 févr. 2012 7:01')
I18nStubber.popFrame()
tz.restore(snapshot)
I18nStubber.clear()
})
test('call the update prop when changed', () => {

View File

@ -19,6 +19,8 @@
import DateHelper from '@canvas/datetime/dateHelper'
import {isDate, isNull, isUndefined} from 'lodash'
import tz from '@canvas/timezone'
import tzInTest from '@canvas/timezone/specHelpers'
import timezone from 'timezone'
import detroit from 'timezone/America/Detroit'
import juneau from 'timezone/America/Juneau'
@ -59,19 +61,28 @@ test('gracefully handles undefined values', () => {
QUnit.module('DateHelper#formatDatetimeForDisplay', {
setup() {
this.snapshot = tz.snapshot()
},
teardown() {
tz.restore(this.snapshot)
tzInTest.restore()
}
})
test('formats the date for display, adjusted for the timezone', () => {
const assignment = defaultAssignment()
tz.changeZone(detroit, 'America/Detroit')
tzInTest.configureAndRestoreLater({
tz: timezone(detroit, 'America/Detroit'),
tzData: {
'America/Detroit': detroit
}
})
let formattedDate = DateHelper.formatDatetimeForDisplay(assignment.due_at)
equal(formattedDate, 'Jul 14, 2015 at 2:35pm')
tz.changeZone(juneau, 'America/Juneau')
tzInTest.configureAndRestoreLater({
tz: timezone(juneau, 'America/Juneau'),
tzData: {
'America/Juneau': juneau
}
})
formattedDate = DateHelper.formatDatetimeForDisplay(assignment.due_at)
equal(formattedDate, 'Jul 14, 2015 at 10:35am')
})
@ -93,37 +104,51 @@ test("can specify 'short' format which excludes the year if it matches the curre
})
QUnit.module('DateHelper#formatDateForDisplay', {
setup() {
this.snapshot = tz.snapshot()
},
teardown() {
tz.restore(this.snapshot)
tzInTest.restore()
}
})
test('formats the date for display, adjusted for the timezone, excluding the time', () => {
const assignment = defaultAssignment()
tz.changeZone(detroit, 'America/Detroit')
tzInTest.configureAndRestoreLater({
tz: timezone(detroit, 'America/Detroit'),
tzData: {
'America/Detroit': detroit
}
})
let formattedDate = DateHelper.formatDateForDisplay(assignment.due_at)
equal(formattedDate, 'Jul 14, 2015')
tz.changeZone(juneau, 'America/Juneau')
tzInTest.configureAndRestoreLater({
tz: timezone(juneau, 'America/Juneau'),
tzData: {
'America/Juneau': juneau
}
})
formattedDate = DateHelper.formatDateForDisplay(assignment.due_at)
equal(formattedDate, 'Jul 14, 2015')
})
QUnit.module('DateHelper#isMidnight', {
setup() {
this.snapshot = tz.snapshot()
},
teardown() {
tz.restore(this.snapshot)
tzInTest.restore()
}
})
test('returns true if the time is midnight, adjusted for the timezone', () => {
const date = '2015-07-14T04:00:00Z'
tz.changeZone(detroit, 'America/Detroit')
tzInTest.configureAndRestoreLater({
tz: timezone(detroit, 'America/Detroit'),
tzData: {
'America/Detroit': detroit
}
})
ok(DateHelper.isMidnight(date))
tz.changeZone(juneau, 'America/Juneau')
tzInTest.configureAndRestoreLater({
tz: timezone(juneau, 'America/Juneau'),
tzData: {
'America/Juneau': juneau
}
})
notOk(DateHelper.isMidnight(date))
})

View File

@ -19,6 +19,8 @@
import $ from 'jquery'
import _ from 'lodash'
import tz from '@canvas/timezone'
import tzInTest from '@canvas/timezone/specHelpers'
import timezone from 'timezone'
import denver from 'timezone/America/Denver'
import newYork from 'timezone/America/New_York'
import SyllabusBehaviors from '@canvas/syllabus/backbone/behaviors/SyllabusBehaviors'
@ -119,9 +121,13 @@ QUnit.module('Syllabus', {
// Setup stubs/mocks
this.server = setupServerResponses()
this.tzSnapshot = tz.snapshot()
tz.changeZone(denver, 'America/Denver')
tz.preload('America/New_York', newYork)
tzInTest.configureAndRestoreLater({
tz: timezone(denver, 'America/Denver'),
tzData: {
'America/Denver': denver,
'America/New_York': newYork
}
})
this.clock = sinon.useFakeTimers(new Date(2012, 0, 23, 15, 30).getTime())
@ -190,7 +196,7 @@ QUnit.module('Syllabus', {
this.miniMonth.remove()
this.jumpToToday.remove()
this.clock.restore()
tz.restore(this.tzSnapshot)
tzInTest.restore()
this.server.restore()
document.getElementById('fixtures').innerHTML = ''
},

View File

@ -23,7 +23,8 @@ import Assignment from '@canvas/assignments/backbone/models/Assignment.coffee'
import Submission from '@canvas/assignments/backbone/models/Submission'
import AssignmentListItemView from 'ui/features/assignment_index/backbone/views/AssignmentListItemView.coffee'
import $ from 'jquery'
import tz from '@canvas/timezone'
import tzInTest from '@canvas/timezone/specHelpers'
import timezone from 'timezone'
import juneau from 'timezone/America/Juneau'
import french from 'timezone/fr_FR'
import I18nStubber from 'helpers/I18nStubber'
@ -169,14 +170,13 @@ QUnit.module('AssignmentListItemViewSpec', {
URLS: {assignment_sort_base_url: 'test'}
})
genSetup.call(this)
this.snapshot = tz.snapshot()
return I18nStubber.pushFrame()
},
teardown() {
fakeENV.teardown()
genTeardown.call(this)
tz.restore(this.snapshot)
return I18nStubber.popFrame()
tzInTest.restore()
return I18nStubber.clear()
}
})
@ -542,7 +542,10 @@ test('does not render score template without permission', function () {
})
test('renders lockAt/unlockAt with locale-appropriate format string', function () {
tz.changeLocale(french, 'fr_FR', 'fr')
tzInTest.configureAndRestoreLater({
tz: timezone(french, 'fr_FR'),
momentLocale: 'fr'
})
I18nStubber.setLocale('fr_FR')
I18nStubber.stub('fr_FR', {
'date.formats.short': '%-d %b',
@ -570,7 +573,12 @@ test('renders lockAt/unlockAt with locale-appropriate format string', function (
})
test('renders lockAt/unlockAt in appropriate time zone', function () {
tz.changeZone(juneau, 'America/Juneau')
tzInTest.configureAndRestoreLater({
tz: timezone(juneau, 'America/Juneau'),
tzData: {
'America/Juneau': juneau
}
})
I18nStubber.stub('en', {
'date.formats.short': '%b %-d',
'date.formats.date_at_time': '%b %-d at %l:%M%P',
@ -646,7 +654,10 @@ test('does not render lockAt/unlockAt when not locking in future', () => {
})
test('renders due date column with locale-appropriate format string', function () {
tz.changeLocale(french, 'fr_FR', 'fr')
tzInTest.configureAndRestoreLater({
tz: timezone(french, 'fr_FR'),
momentLocale: 'fr'
})
I18nStubber.setLocale('fr_FR')
I18nStubber.stub('fr_FR', {
'date.formats.short': '%-d %b',
@ -660,7 +671,12 @@ test('renders due date column with locale-appropriate format string', function (
})
test('renders due date column in appropriate time zone', function () {
tz.changeZone(juneau, 'America/Juneau')
tzInTest.configureAndRestoreLater({
tz: timezone(juneau, 'America/Juneau'),
tzData: {
'America/Juneau': juneau
}
})
I18nStubber.stub('en', {
'date.formats.short': '%b %-d',
'date.abbr_month_names.8': 'Aug'

View File

@ -23,6 +23,8 @@ import CreateAssignmentView from 'ui/features/assignment_index/backbone/views/Cr
import DialogFormView from '@canvas/forms/backbone/views/DialogFormView.coffee'
import $ from 'jquery'
import tz from '@canvas/timezone'
import tzInTest from '@canvas/timezone/specHelpers'
import timezone from 'timezone'
import juneau from 'timezone/America/Juneau'
import french from 'timezone/fr_FR'
import I18nStubber from 'helpers/I18nStubber'
@ -142,14 +144,13 @@ QUnit.module('CreateAssignmentView', {
this.assignment4 = new Assignment(buildAssignment4())
this.assignment5 = new Assignment(buildAssignment5())
this.group = assignmentGroup()
this.snapshot = tz.snapshot()
I18nStubber.pushFrame()
fakeENV.setup()
},
teardown() {
fakeENV.teardown()
tz.restore(this.snapshot)
I18nStubber.popFrame()
tzInTest.restore()
I18nStubber.clear()
}
})
@ -325,12 +326,7 @@ test('openAgain adds datetime picker', function() {
test('adjust datetime to the end of a day for midnight time', function() {
sandbox.stub(DialogFormView.prototype, 'openAgain')
I18nStubber.setLocale('fr_FR')
// tz use the key/values to get formats for different locale
I18nStubber.stub('fr_FR', {
'date.formats.short': '%b %-d',
'time.formats.tiny': '%l:%M%P'
})
I18nStubber.useInitialTranslations()
const tmp = $.screenReaderFlashMessageExclusive
$.screenReaderFlashMessageExclusive = sinon.spy()
const view = createView(this.assignment2)
@ -345,12 +341,7 @@ test('adjust datetime to the end of a day for midnight time', function() {
test('it does not adjust datetime for other date time', function() {
sandbox.stub(DialogFormView.prototype, 'openAgain')
I18nStubber.setLocale('fr_FR')
// tz use the key/values to get formats for different locale
I18nStubber.stub('fr_FR', {
'date.formats.short': '%b %-d',
'time.formats.tiny': '%l:%M%P'
})
I18nStubber.useInitialTranslations()
const tmp = $.screenReaderFlashMessageExclusive
$.screenReaderFlashMessageExclusive = sinon.spy()
const view = createView(this.assignment2)
@ -553,7 +544,10 @@ test('validates due date for lock and unlock', function() {
})
test('renders due dates with locale-appropriate format string', function() {
tz.changeLocale(french, 'fr_FR', 'fr')
tzInTest.configureAndRestoreLater({
tz: timezone(french, 'fr_FR'),
momentLocale: 'fr'
})
I18nStubber.setLocale('fr_FR')
I18nStubber.stub('fr_FR', {
'date.formats.short': '%-d %b',
@ -571,7 +565,13 @@ test('renders due dates with locale-appropriate format string', function() {
})
test('renders due dates in appropriate time zone', function() {
tz.changeZone(juneau, 'America/Juneau')
tzInTest.configureAndRestoreLater({
tz: timezone(juneau, 'America/Juneau'),
tzData: {
'America/Juneau': juneau
}
})
I18nStubber.stub('en', {
'date.formats.short': '%b %-d',
'date.abbr_month_names.8': 'Aug'

View File

@ -667,7 +667,7 @@ test('validates i18n mastery points', function() {
view.$('input[name="mastery_points"]').val('1 234,5')
ok(view.isValid())
view.remove()
I18nStubber.popFrame()
I18nStubber.clear()
})
test('getModifiedFields returns false for all fields when not modified', () => {

View File

@ -19,6 +19,8 @@
import DatetimeField from '@canvas/datetime/jquery/DatetimeField'
import $ from 'jquery'
import tz from '@canvas/timezone'
import tzInTest from '@canvas/timezone/specHelpers'
import timezone from 'timezone'
import detroit from 'timezone/America/Detroit'
import juneau from 'timezone/America/Juneau'
import portuguese from 'timezone/pt_PT'
@ -253,13 +255,12 @@ test('should set suggest text', function() {
QUnit.module('parseValue', {
setup() {
this.snapshot = tz.snapshot()
this.$field = $('<input type="text" name="due_at">')
this.field = new DatetimeField(this.$field, {})
},
teardown() {
tz.restore(this.snapshot)
tzInTest.restore()
}
})
@ -355,7 +356,6 @@ test('interprets time-only fields as occurring on implicit date if set', functio
QUnit.module('updateData', {
setup() {
this.snapshot = tz.snapshot()
tz.changeZone(detroit, 'America/Detroit')
this.$field = $('<input type="text" name="due_at">')
this.$field.val('Jan 1, 1970 at 12:01am')
@ -365,7 +365,7 @@ QUnit.module('updateData', {
},
teardown() {
tz.restore(this.snapshot)
tzInTest.restore()
}
})
@ -403,7 +403,13 @@ test('sets time-* to fudged, 12-hour values', function() {
})
test('sets time-* to fudged, 24-hour values', function() {
tz.changeLocale(portuguese, 'pt_PT', 'pt')
tzInTest.configureAndRestoreLater({
tz: timezone(detroit, 'America/Detroit', portuguese, 'pt_PT'),
tzData: {
'America/Detroit': detroit,
},
momentLocale: 'pt'
})
this.field.updateData()
equal(this.$field.data('time-hour'), '21')
equal(this.$field.data('time-minute'), '56')
@ -573,19 +579,29 @@ test('returns time only if @showDate false', function() {
})
test('localizes formatting of dates and times', function() {
tz.changeLocale(portuguese, 'pt_PT', 'pt')
tzInTest.configureAndRestoreLater({
tz: timezone(detroit, 'America/Detroit', portuguese, 'pt_PT'),
tzData: {
'America/Detroit': detroit,
},
momentLocale: 'pt'
})
I18nStubber.pushFrame()
I18nStubber.setLocale('pt_PT')
I18nStubber.stub('pt_PT', {'date.formats.full_with_weekday': '%a, %-d %b %Y %k:%M'})
equal(this.field.formatSuggest(), 'Dom, 20 Jul 1969 21:56')
I18nStubber.popFrame()
I18nStubber.clear()
})
QUnit.module('formatSuggestCourse', {
setup() {
this.snapshot = tz.snapshot()
tz.changeZone(detroit, 'America/Detroit')
tz.preload('America/Juneau', juneau)
tzInTest.configureAndRestoreLater({
tz: timezone(detroit, 'America/Detroit'),
tzData: {
'America/Detroit': detroit,
'America/Juneau': juneau,
}
})
fakeENV.setup({TIMEZONE: 'America/Detroit', CONTEXT_TIMEZONE: 'America/Juneau'})
this.$field = $('<input type="text" name="due_at">')
this.$field.val('Jul 20, 1969 at 9:56pm')
@ -594,7 +610,7 @@ QUnit.module('formatSuggestCourse', {
teardown() {
fakeENV.teardown()
tz.restore(this.snapshot)
tzInTest.restore()
}
})
@ -719,13 +735,19 @@ test('formats value into val() according to format parameter', function() {
})
test('localizes value', function() {
tz.changeLocale(portuguese, 'pt_PT', 'pt')
tzInTest.configureAndRestoreLater({
tz: timezone(detroit, 'America/Detroit', portuguese, 'pt_PT'),
tzData: {
'America/Detroit': detroit,
},
momentLocale: 'pt'
})
I18nStubber.pushFrame()
I18nStubber.setLocale('pt_PT')
I18nStubber.stub('pt_PT', {'date.formats.full': '%-d %b %Y %-k:%M'})
this.field.setFormattedDatetime(moonwalk, 'date.formats.full')
equal(this.$field.val(), '20 Jul 1969 21:56')
I18nStubber.popFrame()
I18nStubber.clear()
})
QUnit.module('setDate/setTime/setDatetime', {

View File

@ -37,6 +37,7 @@ QUnit.module('EditConferenceView', {
this.view.$el.remove()
fakeENV.teardown()
tz.restore(this.snapshot)
I18nStubber.clear()
}
})
@ -49,7 +50,6 @@ test('updateConferenceUserSettingDetailsForConference localizes values for datep
const conferenceData = {user_settings: {datepickerSetting: '2015-08-07T17:00:00Z'}}
this.view.updateConferenceUserSettingDetailsForConference(conferenceData)
equal(this.datepickerSetting.value, 'ven. 7 août, 2015 17:00')
I18nStubber.popFrame()
})
test('#show sets the proper title for new conferences', function() {

View File

@ -22,6 +22,8 @@ import {mount} from 'enzyme'
import GradingPeriodForm from 'ui/features/account_grading_standards/react/GradingPeriodForm.js'
import chicago from 'timezone/America/Chicago'
import tz from '@canvas/timezone'
import tzInTest from '@canvas/timezone/specHelpers'
import timezone from 'timezone'
import fakeENV from 'helpers/fakeENV'
QUnit.module('GradingPeriodForm', suiteHooks => {
@ -123,10 +125,17 @@ QUnit.module('GradingPeriodForm', suiteHooks => {
QUnit.module('when local and server time are different', hooks => {
hooks.beforeEach(() => {
Object.assign(ENV, {CONTEXT_TIMEZONE: 'America/Chicago'})
tz.preload('America/Chicago', chicago)
tzInTest.configureAndRestoreLater({
tz: timezone('UTC'),
tzData: {
'America/Chicago': chicago
}
})
mountComponent()
})
hooks.afterEach(tzInTest.restore)
test('shows both local and context time suggestions for start date', () => {
strictEqual(getDateTimeSuggestions('Start Date').length, 2)
})
@ -168,10 +177,17 @@ QUnit.module('GradingPeriodForm', suiteHooks => {
QUnit.module('when local and server time are different', hooks => {
hooks.beforeEach(() => {
Object.assign(ENV, {CONTEXT_TIMEZONE: 'America/Chicago'})
tz.preload('America/Chicago', chicago)
tzInTest.configureAndRestoreLater({
tz: timezone('UTC'),
tzData: {
'America/Chicago': chicago
}
})
mountComponent()
})
hooks.afterEach(tzInTest.restore)
test('shows both local and context time suggestions for end date', () => {
strictEqual(getDateTimeSuggestions('End Date').length, 2)
})
@ -213,10 +229,17 @@ QUnit.module('GradingPeriodForm', suiteHooks => {
QUnit.module('when local and server time are different', hooks => {
hooks.beforeEach(() => {
Object.assign(ENV, {CONTEXT_TIMEZONE: 'America/Chicago'})
tz.preload('America/Chicago', chicago)
tzInTest.configureAndRestoreLater({
tz: timezone('UTC'),
tzData: {
'America/Chicago': chicago
}
})
mountComponent()
})
hooks.afterEach(tzInTest.restore)
test('shows both local and context time suggestions for close date', () => {
strictEqual(getDateTimeSuggestions('Close Date').length, 2)
})

View File

@ -1,188 +0,0 @@
/*
* Copyright (C) 2017 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import sinon from 'sinon'
import localeConfig from 'json-loader!yaml-loader!../../../config/locales/locales.yml'
import tz from 'timezone_core'
import DatetimeField from 'compiled/widget/DatetimeField'
import $ from 'jquery'
import I18n from 'i18nObj'
import 'translations/_core'
import 'translations/_core_en'
import bigeasyLocales from 'timezone/locales'
import bigeasyLocale_ca_ES from 'custom_timezone_locales/ca_ES'
import bigeasyLocale_cy_GB from 'custom_timezone_locales/cy_GB'
import bigeasyLocale_de_DE from 'custom_timezone_locales/de_DE'
import bigeasyLocale_fr_FR from 'custom_timezone_locales/fr_FR'
import bigeasyLocale_fr_CA from 'custom_timezone_locales/fr_CA'
import bigeasyLocale_he_IL from 'custom_timezone_locales/he_IL'
import bigeasyLocale_pl_PL from 'custom_timezone_locales/pl_PL'
import bigeasyLocale_is_IS from 'custom_timezone_locales/is_IS'
import bigeasyLocale_ar_SA from 'custom_timezone_locales/ar_SA'
import bigeasyLocale_da_DK from 'custom_timezone_locales/da_DK'
import bigeasyLocale_fa_IR from 'custom_timezone_locales/fa_IR'
import bigeasyLocale_ht_HT from 'custom_timezone_locales/ht_HT'
import bigeasyLocale_hy_AM from 'custom_timezone_locales/hy_AM'
import bigeasyLocale_mi_NZ from 'custom_timezone_locales/mi_NZ'
import bigeasyLocale_nn_NO from 'custom_timezone_locales/nn_NO'
import bigeasyLocale_tr_TR from 'custom_timezone_locales/tr_TR'
import bigeasyLocale_uk_UA from 'custom_timezone_locales/uk_UA'
import bigeasyLocale_el_GR from 'custom_timezone_locales/el_GR'
import 'custom_moment_locales/ca'
import 'custom_moment_locales/de'
import 'custom_moment_locales/he'
import 'custom_moment_locales/pl'
import 'custom_moment_locales/fa'
import 'custom_moment_locales/fr'
import 'custom_moment_locales/fr_ca'
import 'custom_moment_locales/ht_ht'
import 'custom_moment_locales/mi_nz'
import 'custom_moment_locales/hy_am'
import 'custom_moment_locales/sl'
let originalLocale
let originalFallbacksMap
const bigeasyLocalesWithCustom = [
...bigeasyLocales,
bigeasyLocale_ca_ES,
bigeasyLocale_cy_GB,
bigeasyLocale_de_DE,
bigeasyLocale_fr_FR,
bigeasyLocale_fr_CA,
bigeasyLocale_he_IL,
bigeasyLocale_pl_PL,
bigeasyLocale_is_IS,
bigeasyLocale_ar_SA,
bigeasyLocale_da_DK,
bigeasyLocale_fa_IR,
bigeasyLocale_ht_HT,
bigeasyLocale_hy_AM,
bigeasyLocale_mi_NZ,
bigeasyLocale_nn_NO,
bigeasyLocale_tr_TR,
bigeasyLocale_uk_UA,
bigeasyLocale_el_GR
]
const preloadedData = bigeasyLocalesWithCustom.reduce((memo, locale) => {
memo[locale.name] = locale
return memo
}, {})
QUnit.module('Parsing locale formatted dates', {
setup() {
originalLocale = I18n.locale
sinon.stub(tz, 'preload').callsFake(name => preloadedData[name])
originalFallbacksMap = I18n.fallbacksMap
I18n.fallbacksMap = null
},
teardown() {
I18n.locale = originalLocale
I18n.fallbacksMap = originalFallbacksMap
tz.preload.restore()
}
})
const locales = Object.keys(localeConfig)
.map(key => {
const locale = localeConfig[key]
const base = key.split('-')[0]
return {
key,
moment: locale.moment_locale || key.toLowerCase(),
bigeasy: locale.bigeasy_locale || localeConfig[base].bigeasy_locale
}
})
.filter(l => l.key)
const dates = []
const currentYear = parseInt(tz.format(new Date(), '%Y'), 10)
const otherYear = currentYear + 4
for (let i = 0; i < 12; ++i) {
dates.push(new Date(Date.UTC(currentYear, i, 1, 23, 59)))
dates.push(new Date(Date.UTC(currentYear, i, 28, 23, 59)))
dates.push(new Date(Date.UTC(otherYear, i, 7, 23, 59)))
dates.push(new Date(Date.UTC(otherYear, i, 15, 23, 59)))
}
function assertFormattedParsesToDate(formatted, date) {
const parsed = tz.parse(formatted)
const formattedDate = tz.format(parsed, 'date.formats.medium')
const formattedTime = tz.format(parsed, 'time.formats.tiny')
const formattedParsed = `${formattedDate} ${formattedTime}`
equal(date.getTime(), parsed.getTime(), `${formatted} incorrectly parsed as ${formattedParsed}`)
}
locales.forEach(locale => {
test(`timezone -> moment for ${locale.key}`, () => {
I18n.locale = locale.key
try {
tz.changeLocale(locale.bigeasy, locale.moment)
dates.forEach(date => {
const formattedDate = $.dateString(date)
const formattedTime = tz.format(date, 'time.formats.tiny')
const formatted = `${formattedDate} ${formattedTime}`
assertFormattedParsesToDate(formatted, date)
})
} catch (err) {
ok(false, err.message)
}
})
test(`datepicker -> moment for ${locale.key}`, () => {
I18n.locale = locale.key
const config = DatetimeField.prototype.datepickerDefaults()
try {
tz.changeLocale(locale.bigeasy, locale.moment)
dates.forEach(date => {
const formattedDate = $.datepicker.formatDate(config.dateFormat, date, config)
const formattedTime = $.timeString(date)
const formatted = `${formattedDate} ${formattedTime}`
assertFormattedParsesToDate(formatted, date)
})
} catch (err) {
ok(false, err.message)
}
})
test(`hour format matches timezone locale for ${locale.key}`, () => {
I18n.locale = locale.key
tz.changeLocale(locale.bigeasy, locale.moment)
const formats = [
'date.formats.date_at_time',
'date.formats.full',
'date.formats.full_with_weekday',
'time.formats.tiny',
'time.formats.tiny_on_the_hour'
]
const invalid = key => {
const format = I18n.lookup(key)
// ok(/%p/i.test(format) === tz.hasMeridian(), `format: ${format}, hasMeridian: ${tz.hasMeridian()}`)
ok(
tz.hasMeridian() || !/%p/i.test(format),
`format: ${format}, hasMeridian: ${tz.hasMeridian()}`
)
}
ok(!formats.forEach(invalid))
})
})

View File

@ -16,8 +16,10 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import moment_formats from '@canvas/timezone/moment_formats'
import I18nStubber from 'helpers/I18nStubber'
import {
prepareFormats
} from '../../../ui/boot/initializers/configureDateTimeMomentParser'
QUnit.module('Moment formats', {
setup() {
@ -32,18 +34,18 @@ QUnit.module('Moment formats', {
})
},
teardown() {
I18nStubber.popFrame()
I18nStubber.clear()
}
})
test('formatsForLocale include formats matching datepicker', () => {
const formats = moment_formats.formatsForLocale()
const formats = prepareFormats().map(x => x())
ok(formats.includes('%b %-d, %Y %l:%M%P'))
ok(formats.includes('%b %-d, %Y %l%P'))
})
test('formatsForLocale includes all event formats', () => {
const formats = moment_formats.formatsForLocale()
const formats = prepareFormats().map(x => x())
ok(formats.includes('%b %-d, %Y event %l:%M%P'))
ok(formats.includes('%b %-d event %l:%M%P'))
ok(formats.includes('%b %-d, %Y event %l%P'))

View File

@ -45,7 +45,7 @@ QUnit.module('Number Helper Parse and Validate', {
},
teardown() {
I18nStubber.popFrame()
I18nStubber.clear()
if (numberHelper._parseNumber.restore) {
numberHelper._parseNumber.restore()
}

View File

@ -0,0 +1,25 @@
/*
* Copyright (C) 2021 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import tz from '@canvas/timezone'
import tzInTest from '@canvas/timezone/specHelpers'
tz.snapshot = () => {}
tz.restore = tzInTest.restore
tz.changeZone = tzInTest.changeZone
tz.changeLocale = tzInTest.changeLocale

View File

@ -1,651 +0,0 @@
//
// Copyright (C) 2013 - present Instructure, Inc.
//
// This file is part of Canvas.
//
// Canvas is free software: you can redistribute it and/or modify it under
// the terms of the GNU Affero General Public License as published by the Free
// Software Foundation, version 3 of the License.
//
// Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
// A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
// details.
//
// You should have received a copy of the GNU Affero General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>.
import tz from '@canvas/timezone'
import i18nObj from '@canvas/i18n'
import detroit from 'timezone/America/Detroit'
import french from 'timezone/fr_FR'
import portuguese from 'timezone/pt_PT'
import chinese from 'timezone/zh_CN'
import I18nStubber from 'helpers/I18nStubber'
import trans from 'translations/_core_en'
import en_US from 'timezone/en_US'
import MockDate from 'mockdate'
QUnit.module('timezone', {
setup() {
this.snapshot = tz.snapshot()
I18nStubber.pushFrame()
},
teardown() {
tz.restore(this.snapshot)
I18nStubber.popFrame()
}
})
const moonwalk = new Date(Date.UTC(1969, 6, 21, 2, 56))
const epoch = new Date(Date.UTC(1970, 0, 1, 0, 0))
test('moment(one-arg) complains', () => {
let err = null
try {
tz.moment('June 24 at 10:00pm')
} catch (error) {
err = error
}
ok(err.toString().match(/^Error: tz.moment only works on /))
})
test('moment(non-string, fmt-string) complains', () => {
let err = null
try {
tz.moment(moonwalk, 'MMMM D h:mmA')
} catch (error) {
err = error
}
ok(err.toString().match(/^Error: tz.moment only works on /))
})
test('moment(date-string, non-string) complains', () => {
let err = null
try {
tz.moment('June 24 at 10:00pm', 123)
} catch (error) {
err = error
}
ok(err.toString().match(/^Error: tz.moment only works on /))
})
test('moment(date-string, fmt-string) works', () =>
ok(tz.moment('June 24 at 10:00pm', 'MMMM D h:mmA')))
test('moment(date-string, [fmt-strings]) works', () =>
ok(tz.moment('June 24 at 10:00pm', ['MMMM D h:mmA', 'L'])))
test('moment passes through invalid results', () => {
const m = tz.moment('not a valid date', 'L')
ok(!m.isValid())
})
test('moment accepts excess input, but all format used', () => {
const m = tz.moment('12pm and more', 'ha')
ok(m.isValid())
})
test('moment rejects excess format', () => {
const m = tz.moment('12pm', 'h:mma')
ok(!m.isValid())
})
test('moment returns moment for valid results', () => {
const m = tz.moment('June 24, 2015 at 10:00pm -04:00', 'MMMM D, YYYY h:mmA Z')
ok(m.isValid())
})
test('moment sans-timezone info parses according to profile timezone', () => {
tz.changeZone(detroit, 'America/Detroit')
const expected = new Date(1435197600000) // 10pm EDT on June 24, 2015
const m = tz.moment('June 24, 2015 at 10:00pm', 'MMMM D, YYYY h:mmA')
equal(+m.toDate(), +expected)
})
test('moment with-timezone info parses according to that timezone', () => {
tz.changeZone(detroit, 'America/Detroit')
const expected = new Date(1435204800000) // 10pm MDT on June 24, 2015
const m = tz.moment('June 24, 2015 at 10:00pm -06:00', 'MMMM D, YYYY h:mmA Z')
equal(+m.toDate(), +expected)
})
test('moment can change locales with single arity', () => {
tz.changeLocale('en_US', 'en')
const m1 = tz.moment('mercredi 1 juillet 2015 15:00', 'LLLL')
ok(!m1._locale._abbr.match(/fr/))
ok(!m1.isValid())
tz.changeLocale('fr_FR', 'fr')
const m2 = tz.moment('mercredi 1 juillet 2015 15:00', 'LLLL')
ok(m2._locale._abbr.match(/fr/))
ok(m2.isValid())
})
test('moment can change locales with multiple arity', () => {
tz.changeLocale('en_US', 'en')
const m1 = tz.moment('mercredi 1 juillet 2015 15:00', 'LLLL')
ok(!m1._locale._abbr.match(/fr/))
ok(!m1.isValid())
tz.changeLocale(french, 'fr_FR', 'fr')
const m2 = tz.moment('mercredi 1 juillet 2015 15:00', 'LLLL')
ok(m2._locale._abbr.match(/fr/))
ok(m2.isValid())
})
test('parse(valid datetime string)', () => {
equal(+tz.parse(moonwalk.toISOString()), +moonwalk)
})
test('parse(timestamp integer)', () => {
equal(+tz.parse(+moonwalk), +moonwalk)
})
test('parse(Date object)', () => {
equal(+tz.parse(moonwalk), +moonwalk)
})
test('parse(date array)', () => {
equal(+tz.parse([1969, 7, 21, 2, 56]), +moonwalk)
})
test('parse() should return null on failure', () => equal(tz.parse('bogus'), null))
test('parse() should return a date on success', () => equal(typeof tz.parse(+moonwalk), 'object'))
test('parse("") should fail', () => equal(tz.parse(''), null))
test('parse(null) should fail', () => equal(tz.parse(null), null))
test('parse(integer) should be ms since epoch', () => equal(+tz.parse(2016), +tz.raw_parse(2016)))
test('parse("looks like integer") should be a year', () =>
equal(+tz.parse('2016'), +tz.parse('2016-01-01')))
test('parse() should parse relative to UTC by default', () =>
equal(+tz.parse('1969-07-21 02:56'), +moonwalk))
test('format() should format relative to UTC by default', () =>
equal(tz.format(moonwalk, '%F %T%:z'), '1969-07-21 02:56:00+00:00'))
test('format() should format in en_US by default', () =>
equal(tz.format(moonwalk, '%c'), 'Mon 21 Jul 1969 02:56:00 AM UTC'))
test('format() should parse the value if necessary', () =>
equal(tz.format('1969-07-21 02:56', '%F %T%:z'), '1969-07-21 02:56:00+00:00'))
test('format() should return null if the parse fails', () =>
equal(tz.format('bogus', '%F %T%:z'), null))
test('format() should return null if the format string is unrecognized', () =>
equal(tz.format(moonwalk, 'bogus'), null))
test('format() should preserve 12-hour+am/pm if the locale does define am/pm', () => {
const time = tz.parse('1969-07-21 15:00:00')
equal(tz.format(time, '%-l%P'), '3pm')
equal(tz.format(time, '%I%P'), '03pm')
equal(tz.format(time, '%r'), '03:00:00 PM')
})
test("format() should promote 12-hour+am/pm into 24-hour if the locale doesn't define am/pm", () => {
const time = tz.parse('1969-07-21 15:00:00')
tz.changeLocale(french, 'fr_FR', 'fr')
equal(tz.format(time, '%-l%P'), '15')
equal(tz.format(time, '%I%P'), '15')
equal(tz.format(time, '%r'), '15:00:00')
})
test('format() should recognize date.formats.*', () => {
I18nStubber.stub('en', {'date.formats.short': '%b %-d'})
equal(tz.format(moonwalk, 'date.formats.short'), 'Jul 21')
})
test('format() should recognize time.formats.*', () => {
I18nStubber.stub('en', {'time.formats.tiny': '%-l:%M%P'})
equal(tz.format(epoch, 'time.formats.tiny'), '12:00am')
})
test('format() should localize when given a localization key', () => {
tz.changeLocale(french, 'fr_FR', 'fr')
I18nStubber.setLocale('fr_FR')
I18nStubber.stub('fr_FR', {'date.formats.full': '%-d %b %Y %-l:%M%P'})
equal(tz.format(moonwalk, 'date.formats.full'), '21 juil. 1969 2:56')
})
test('format() should automatically convert %l to %-l when given a localization key', () => {
I18nStubber.stub('en', {'time.formats.tiny': '%l:%M%P'})
equal(tz.format(moonwalk, 'time.formats.tiny'), '2:56am')
})
test('format() should automatically convert %k to %-k when given a localization key', () => {
I18nStubber.stub('en', {'time.formats.tiny': '%k:%M'})
equal(tz.format(moonwalk, 'time.formats.tiny'), '2:56')
})
test('format() should automatically convert %e to %-e when given a localization key', () => {
I18nStubber.stub('en', {'date.formats.short': '%b %e'})
equal(tz.format(epoch, 'date.formats.short'), 'Jan 1')
})
test('shift() should adjust the date as appropriate', () =>
equal(+tz.shift(moonwalk, '-1 day'), moonwalk - 86400000))
test('shift() should apply multiple directives', () =>
equal(+tz.shift(moonwalk, '-1 day', '-1 hour'), moonwalk - 86400000 - 3600000))
test('shift() should parse the value if necessary', () =>
equal(+tz.shift('1969-07-21 02:56', '-1 day'), moonwalk - 86400000))
test('shift() should return null if the parse fails', () =>
equal(tz.shift('bogus', '-1 day'), null))
test('shift() should return null if the directives includes a format string', () =>
equal(tz.shift('bogus', '-1 day', '%F %T%:z'), null))
test('extendConfiguration() should curry the options into tz', () => {
tz.extendConfiguration(detroit, 'America/Detroit')
equal(+tz.parse('1969-07-20 21:56'), +moonwalk)
equal(tz.format(moonwalk, '%c'), 'Sun 20 Jul 1969 09:56:00 PM EST')
})
test('snapshotting should let you restore tz to a previous un-curried state', () => {
const snapshot = tz.snapshot()
tz.extendConfiguration(detroit, 'America/Detroit')
tz.restore(snapshot)
equal(+tz.parse('1969-07-21 02:56'), +moonwalk)
equal(tz.format(moonwalk, '%c'), 'Mon 21 Jul 1969 02:56:00 AM UTC')
})
test('changeZone(...) should synchronously curry in a loaded zone', () => {
tz.changeZone(detroit, 'America/Detroit')
equal(+tz.parse('1969-07-20 21:56'), +moonwalk)
equal(tz.format(moonwalk, '%c'), 'Sun 20 Jul 1969 09:56:00 PM EST')
})
test('changeZone(...) should asynchronously curry in a zone by name', assert => {
const done = assert.async()
assert.expect(2)
tz.changeZone('America/Detroit').then(() => {
equal(+tz.parse('1969-07-20 21:56'), +moonwalk)
equal(tz.format(moonwalk, '%c'), 'Sun 20 Jul 1969 09:56:00 PM EST')
done()
})
})
test('changeLocale(...) should synchronously curry in a loaded locale', () => {
tz.changeLocale(french, 'fr_FR', 'fr')
equal(tz.format(moonwalk, '%c'), 'lun. 21 juil. 1969 02:56:00 UTC')
})
test('changeLocale(...) should asynchronously curry in a locale by name', assert => {
const done = assert.async()
assert.expect(1)
tz.changeLocale('fr_FR', 'fr').then(() => {
equal(tz.format(moonwalk, '%c'), 'lun. 21 juil. 1969 02:56:00 UTC')
done()
})
})
test('changeZone(...) should synchronously curry if pre-loaded', () => {
tz.preload('America/Detroit', detroit)
tz.changeZone('America/Detroit')
equal(tz.format(moonwalk, '%c'), 'Sun 20 Jul 1969 09:56:00 PM EST')
})
test('hasMeridian() true if locale defines am/pm', () => ok(tz.hasMeridian()))
test("hasMeridian() false if locale doesn't define am/pm", () => {
tz.changeLocale(french, 'fr_FR', 'fr')
ok(!tz.hasMeridian())
})
test('useMeridian() true if locale defines am/pm and uses 12-hour format', () => {
I18nStubber.stub('en', {'time.formats.tiny': '%l:%M%P'})
ok(tz.hasMeridian())
ok(tz.useMeridian())
})
test('useMeridian() false if locale defines am/pm but uses 24-hour format', () => {
I18nStubber.stub('en', {'time.formats.tiny': '%k:%M'})
ok(tz.hasMeridian())
ok(!tz.useMeridian())
})
test("useMeridian() false if locale doesn't define am/pm and instead uses 24-hour format", () => {
tz.changeLocale(french, 'fr_FR', 'fr')
I18nStubber.setLocale('fr_FR')
I18nStubber.stub('fr_FR', {'time.formats.tiny': '%-k:%M'})
ok(!tz.hasMeridian())
ok(!tz.useMeridian())
})
test("useMeridian() false if locale doesn't define am/pm but still uses 12-hour format (format will be corrected)", () => {
tz.changeLocale(french, 'fr_FR', 'fr')
I18nStubber.setLocale('fr_FR')
I18nStubber.stub('fr_FR', {'time.formats.tiny': '%-l:%M%P'})
ok(!tz.hasMeridian())
ok(!tz.useMeridian())
})
test('isMidnight() is false when no argument given.', () => ok(!tz.isMidnight()))
test('isMidnight() is false when invalid date is given.', () => {
const date = new Date('invalid date')
ok(!tz.isMidnight(date))
})
test('isMidnight() is true when date given is at midnight.', () => ok(tz.isMidnight(epoch)))
test("isMidnight() is false when date given isn't at midnight.", () => ok(!tz.isMidnight(moonwalk)))
test('isMidnight() is false when date is midnight in a different zone.', () => {
tz.changeZone(detroit, 'America/Detroit')
ok(!tz.isMidnight(epoch))
})
test('changeToTheSecondBeforeMidnight() returns null when no argument given.', () =>
equal(tz.changeToTheSecondBeforeMidnight(), null))
test('changeToTheSecondBeforeMidnight() returns null when invalid date is given.', () => {
const date = new Date('invalid date')
equal(tz.changeToTheSecondBeforeMidnight(date), null)
})
test('changeToTheSecondBeforeMidnight() returns fancy midnight when a valid date is given.', () => {
const fancyMidnight = tz.changeToTheSecondBeforeMidnight(epoch)
equal(fancyMidnight.toGMTString(), 'Thu, 01 Jan 1970 23:59:59 GMT')
})
test('mergeTimeAndDate() finds the given time of day on the given date.', () =>
equal(+tz.mergeTimeAndDate(moonwalk, epoch), +new Date(Date.UTC(1970, 0, 1, 2, 56))))
QUnit.module('english tz', {
setup() {
MockDate.set('2015-02-01', 'UTC')
this.snapshot = tz.snapshot()
I18nStubber.pushFrame()
I18nStubber.setLocale('en_US')
I18nStubber.stub('en_US', {
'date.formats.date_at_time': '%b %-d at %l:%M%P',
'date.formats.default': '%Y-%m-%d',
'date.formats.full': '%b %-d, %Y %-l:%M%P',
'date.formats.full_with_weekday': '%a %b %-d, %Y %-l:%M%P',
'date.formats.long': '%B %-d, %Y',
'date.formats.long_with_weekday': '%A, %B %-d',
'date.formats.medium': '%b %-d, %Y',
'date.formats.medium_month': '%b %Y',
'date.formats.medium_with_weekday': '%a %b %-d, %Y',
'date.formats.short': '%b %-d',
'date.formats.short_month': '%b',
'date.formats.short_weekday': '%a',
'date.formats.short_with_weekday': '%a, %b %-d',
'date.formats.weekday': '%A',
'time.formats.default': '%a, %d %b %Y %H:%M:%S %z',
'time.formats.long': '%B %d, %Y %H:%M',
'time.formats.short': '%d %b %H:%M',
'time.formats.tiny': '%l:%M%P',
'time.formats.tiny_on_the_hour': '%l%P'
})
tz.changeLocale('en_US', 'en')
},
teardown() {
tz.restore(this.snapshot)
I18nStubber.popFrame()
MockDate.reset()
}
})
test('parses english dates', () => {
const engDates = [
'08/03/2015',
'8/3/2015',
'August 3, 2015',
'Aug 3, 2015',
'3 Aug 2015',
'2015-08-03',
'2015 08 03',
'August 3, 2015',
'Monday, August 3',
'Mon Aug 3, 2015',
'Mon, Aug 3',
'Aug 3'
]
engDates.forEach(date => {
const d = tz.parse(date)
equal(tz.format(d, '%d'), '03', `this works: ${date}`)
})
})
test('parses english times', () => {
const engTimes = ['6:06 PM', '6:06:22 PM', '6:06pm', '6pm']
engTimes.forEach(time => {
const d = tz.parse(time)
equal(tz.format(d, '%H'), '18', `this works: ${time}`)
})
})
test('parses english date times', () => {
const engDateTimes = [
'2015-08-03 18:06:22',
'August 3, 2015 6:06 PM',
'Aug 3, 2015 6:06 PM',
'Aug 3, 2015 6pm',
'Monday, August 3, 2015 6:06 PM',
'Mon, Aug 3, 2015 6:06 PM',
'Aug 3 at 6:06pm',
'Aug 3, 2015 6:06pm',
'Mon Aug 3, 2015 6:06pm'
]
engDateTimes.forEach(dateTime => {
const d = tz.parse(dateTime)
equal(tz.format(d, '%d %H'), '03 18', `this works: ${dateTime}`)
})
})
test('parses 24hr times even if the locale lacks them', () => {
const d = tz.parse('18:06')
equal(tz.format(d, '%H:%M'), '18:06')
})
QUnit.module('french tz', {
setup() {
MockDate.set('2015-02-01', 'UTC')
this.snapshot = tz.snapshot()
I18nStubber.pushFrame()
I18nStubber.setLocale('fr_FR')
I18nStubber.stub('fr_FR', {
'date.formats.date_at_time': '%-d %b à %k:%M',
'date.formats.default': '%d/%m/%Y',
'date.formats.full': '%b %-d, %Y %-k:%M',
'date.formats.full_with_weekday': '%a %-d %b, %Y %-k:%M',
'date.formats.long': 'le %-d %B %Y',
'date.formats.long_with_weekday': '%A, %-d %B',
'date.formats.medium': '%-d %b %Y',
'date.formats.medium_month': '%b %Y',
'date.formats.medium_with_weekday': '%a %-d %b %Y',
'date.formats.short': '%-d %b',
'date.formats.short_month': '%b',
'date.formats.short_weekday': '%a',
'date.formats.short_with_weekday': '%a, %-d %b',
'date.formats.weekday': '%A',
'time.formats.default': '%a, %d %b %Y %H:%M:%S %z',
'time.formats.long': ' %d %B, %Y %H:%M',
'time.formats.short': '%d %b %H:%M',
'time.formats.tiny': '%k:%M',
'time.formats.tiny_on_the_hour': '%k:%M'
})
tz.changeLocale(french, 'fr_FR', 'fr')
},
teardown() {
tz.restore(this.snapshot)
I18nStubber.popFrame()
MockDate.reset()
}
})
test('parses french dates', () => {
const frenchDates = [
'03/08/2015',
'3/8/2015',
'3 août 2015',
'2015-08-03',
'le 3 août 2015',
'lundi, 3 août',
'lun. 3 août 2015',
'3 août',
'lun., 3 août',
'3 août 2015',
'3 août'
]
frenchDates.forEach(date => {
const d = tz.parse(date)
equal(tz.format(d, '%d'), '03', `this works: ${date}`)
})
})
test('parses french times', () => {
const frenchTimes = ['18:06', '18:06:22']
frenchTimes.forEach(time => {
const d = tz.parse(time)
equal(tz.format(d, '%H'), '18', `this works: ${time}`)
})
})
test('parses french date times', () => {
const frenchDateTimes = [
'2015-08-03 18:06:22',
'3 août 2015 18:06',
'lundi 3 août 2015 18:06',
'lun. 3 août 2015 18:06',
'3 août à 18:06',
'août 3, 2015 18:06',
'lun. 3 août, 2015 18:06'
]
frenchDateTimes.forEach(dateTime => {
const d = tz.parse(dateTime)
equal(tz.format(d, '%d %H'), '03 18', `this works: ${dateTime}`)
})
})
QUnit.module('chinese tz', {
setup() {
MockDate.set('2015-02-01', 'UTC')
this.snapshot = tz.snapshot()
I18nStubber.pushFrame()
I18nStubber.setLocale('zh_CN')
I18nStubber.stub('zh_CN', {
'date.formats.date_at_time': '%b %-d 于 %H:%M',
'date.formats.default': '%Y-%m-%d',
'date.formats.full': '%b %-d, %Y %-l:%M%P',
'date.formats.full_with_weekday': '%a %b %-d, %Y %-l:%M%P',
'date.formats.long': '%Y %B %-d',
'date.formats.long_with_weekday': '%A, %B %-d',
'date.formats.medium': '%Y %b %-d',
'date.formats.medium_month': '%Y %b',
'date.formats.medium_with_weekday': '%a %Y %b %-d',
'date.formats.short': '%b %-d',
'date.formats.short_month': '%b',
'date.formats.short_weekday': '%a',
'date.formats.short_with_weekday': '%a, %b %-d',
'date.formats.weekday': '%A',
'time.formats.default': '%a, %Y %b %d %H:%M:%S %z',
'time.formats.long': '%Y %B %d %H:%M',
'time.formats.short': '%b %d %H:%M',
'time.formats.tiny': '%H:%M',
'time.formats.tiny_on_the_hour': '%k:%M'
})
tz.changeLocale(chinese, 'zh_CN', 'zh-cn')
},
teardown() {
tz.restore(this.snapshot)
I18nStubber.popFrame()
MockDate.reset()
}
})
test('parses chinese dates', () => {
const chineseDates = [
'2015-08-03',
'2015年8月3日',
'2015 八月 3',
'2015 8月 3',
'星期一, 八月 3',
'一 2015 8月 3',
'一, 8月 3',
'8月 3'
]
chineseDates.forEach(date => {
const d = tz.parse(date)
equal(tz.format(d, '%d'), '03', `this works: ${date}`)
})
})
test('parses chinese PM times', () => {
// 晚上 means evening. moment doesn't seem to be handling that correctly,
// though I don't believe this will cause any problems in canvas.
const chineseTimes = [
// '晚上6点06分',
// '晚上6点6分22秒',
'18:06'
]
chineseTimes.forEach(time => {
const d = tz.parse(time)
equal(tz.format(d, '%H'), '18', `this works: ${time}`)
})
})
// the 2 chinese AM specs pass in isolation, but when run as part of the whole js test suite
// with COVERAGE=1, which serializes the tests, it fails. I suspect it's due to
// pollution from another test, but cannot find it.
// Skipping for now so the master build completes w/o error.
QUnit.skip('parses chinese AM times', () => {
const chineseTimes = ['6点06分', '6点6分22秒', '06:06']
chineseTimes.forEach(time => {
const d = tz.parse(time)
equal(tz.format(d, '%H'), '06', `this works: ${time}`)
})
})
QUnit.skip('parses chinese date AM times', () => {
const chineseDateTimes = [
'2015-08-03 06:06:22',
'2015年8月3日6点06分',
'2015年8月3日星期一6点06分',
'8月 3日 于 6:06',
'20158月3日, 6:06',
'一 20158月3日, 6:06' // this is incorrectly parsing as "Fri, 20 Mar 1908 06:06:00 GMT"
]
chineseDateTimes.forEach(dateTime => {
const d = tz.parse(dateTime)
equal(tz.format(d, '%d %H'), '03 06', `this works: ${dateTime}`)
})
})
test('parses chinese date PM times', () => {
const chineseDateTimes = [
'2015-08-03 18:06:22',
'2015年8月3日晚上6点06分',
// '2015年8月3日星期一晚上6点06分', // parsing as "Mon, 03 Aug 2015 06:06:00 GMT"
'8月 3日, 于 18:06'
// '2015 8月 3日, 6:06下午', // doesn't recognize 下午 as implying PM
// '一 2015 8月 3日, 6:06下午'
]
chineseDateTimes.forEach(dateTime => {
const d = tz.parse(dateTime)
equal(tz.format(d, '%d %H'), '03 18', `this works: ${dateTime}`)
})
})

View File

@ -22,8 +22,14 @@ import en_US from 'timezone/en_US'
import './jsx/spec-support/specProtection'
import setupRavenConsoleLoggingPlugin from '../../ui/boot/initializers/setupRavenConsoleLoggingPlugin.js'
import {filterUselessConsoleMessages} from '@instructure/js-utils'
import './jsx/spec-support/timezoneBackwardsCompatLayer'
import {
up as configureDateTimeMomentParser
} from '../../ui/boot/initializers/configureDateTimeMomentParser'
filterUselessConsoleMessages(console)
configureDateTimeMomentParser()
Enzyme.configure({adapter: new Adapter()})

View File

@ -21,18 +21,19 @@ import canvasHighContrastTheme from '@instructure/canvas-high-contrast-theme'
import moment from 'moment'
import tz from '@canvas/timezone'
import './initializers/fakeRequireJSFallback.js'
import {
up as configureDateTimeMomentParser
} from './initializers/configureDateTimeMomentParser'
import {
up as configureTimezone
} from './initializers/configureTimezone'
// we already put a <script> tag for the locale corresponding ENV.MOMENT_LOCALE
// on the page from rails, so this should not cause a new network request.
moment().locale(ENV.MOMENT_LOCALE)
// These timezones and locales should already be put on the page as <script>
// tags from rails. this block should not create any network requests.
if (typeof ENV !== 'undefined') {
if (ENV.TIMEZONE) tz.changeZone(ENV.TIMEZONE)
if (ENV.CONTEXT_TIMEZONE) tz.preload(ENV.CONTEXT_TIMEZONE)
if (ENV.BIGEASY_LOCALE) tz.changeLocale(ENV.BIGEASY_LOCALE, ENV.MOMENT_LOCALE)
}
configureDateTimeMomentParser()
configureTimezone()
// This will inject and set up sentry for deprecation reporting. It should be
// stripped out and be a no-op in production.

View File

@ -0,0 +1,79 @@
/*
* Copyright (C) 2021 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import I18n from 'i18n!instructure'
import { useI18nFormats } from 'date-time-moment-parser'
const dateFormat = key => () => (
I18n.lookup(`date.formats.${key}`)
)
const eventFormat = ({ date, time }) => () => (
I18n.t('#time.event', '%{date} at %{time}', {
date: I18n.lookup(`date.formats.${date}`),
time: I18n.lookup(`time.formats.${time}`)
})
)
const timeFormat = key => () => (
I18n.lookup(`time.formats.${key}`)
)
const joinFormats = (separator, formats) => () => (
formats.map(key => I18n.lookup(key)).join(separator)
)
export function prepareFormats() {
// examples are from en_US. order is significant since if an input matches
// multiple formats, the format earlier in the list will be preferred
return [
timeFormat('default'), // %a, %d %b %Y %H:%M:%S %z
dateFormat('full_with_weekday'), // %a %b %-d, %Y %-l:%M%P
dateFormat('full'), // %b %-d, %Y %-l:%M%P
dateFormat('date_at_time'), // %b %-d at %l:%M%P
dateFormat('long_with_weekday'), // %A, %B %-d
dateFormat('medium_with_weekday'), // %a %b %-d, %Y
dateFormat('short_with_weekday'), // %a, %b %-d
timeFormat('long'), // %B %d, %Y %H:%M
dateFormat('long'), // %B %-d, %Y
eventFormat({ date: 'medium', time: 'tiny' }),
eventFormat({ date: 'medium', time: 'tiny_on_the_hour' }),
eventFormat({ date: 'short', time: 'tiny' }),
eventFormat({ date: 'short', time: 'tiny_on_the_hour' }),
joinFormats(' ', ['date.formats.medium', 'time.formats.tiny']),
joinFormats(' ', ['date.formats.medium', 'time.formats.tiny_on_the_hour']),
dateFormat('medium'), // %b %-d, %Y
timeFormat('short'), // %d %b %H:%M
joinFormats(' ', ['date.formats.short', 'time.formats.tiny']),
joinFormats(' ', ['date.formats.short', 'time.formats.tiny_on_the_hour']),
dateFormat('short'), // %b %-d
dateFormat('default'), // %Y-%m-%d
timeFormat('tiny'), // %l:%M%P
timeFormat('tiny_on_the_hour'), // %l%P
dateFormat('weekday'), // %A
dateFormat('short_weekday') // %a
]
}
export function up() {
useI18nFormats(prepareFormats())
}
export function down() {
useI18nFormats([])
}

View File

@ -0,0 +1,47 @@
/*
* Copyright (C) 2021 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { configure } from '@canvas/timezone'
import timezone from 'timezone'
import en_US from 'timezone/en_US'
export function up() {
const tzData = window.__PRELOADED_TIMEZONE_DATA__ || {}
let userTZ = timezone(en_US, 'en_US', 'UTC')
// These timezones and locales should already be put on the page as <script>
// tags from rails. this block should not create any network requests.
if (window.ENV && ENV.TIMEZONE && tzData[ENV.TIMEZONE]) {
userTZ = userTZ(tzData[ENV.TIMEZONE], ENV.TIMEZONE)
}
if (window.ENV && ENV.BIGEASY_LOCALE && tzData[ENV.BIGEASY_LOCALE]) {
userTZ = userTZ(tzData[ENV.BIGEASY_LOCALE], ENV.BIGEASY_LOCALE)
}
configure({
tz: userTZ,
tzData,
momentLocale: window.ENV && ENV.MOMENT_LOCALE || 'en',
})
}
export function down() {
configure({})
}

View File

@ -18,7 +18,9 @@
import React from 'react'
import {render, fireEvent, act} from '@testing-library/react'
import timezone from '@canvas/timezone'
import tz from 'timezone'
import timezone, { configure as configureTimezone } from '@canvas/timezone'
import tzInTest from '@canvas/timezone/specHelpers'
import tokyo from 'timezone/Asia/Tokyo'
import anchorage from 'timezone/America/Anchorage'
import moment from 'moment-timezone'
@ -129,21 +131,24 @@ beforeEach(() => {
describe('Assignment Bulk Edit Dates', () => {
let oldEnv
let timezoneSnapshot
beforeEach(() => {
oldEnv = window.ENV
window.ENV = {
TIMEZONE: 'Asia/Tokyo',
FEATURES: {}
}
timezoneSnapshot = timezone.snapshot()
timezone.changeZone(tokyo, 'Asia/Tokyo')
tzInTest.configureAndRestoreLater({
tz: tz(tokyo, 'Asia/Tokyo'),
tzData: {
'Asia/Tokyo': tokyo
}
})
})
afterEach(async () => {
await flushPromises()
window.ENV = oldEnv
timezone.restore(timezoneSnapshot)
tzInTest.restore()
})
it('shows a spinner while loading', async () => {
@ -1020,21 +1025,25 @@ describe('Assignment Bulk Edit Dates', () => {
describe('in a timezone that does DST', () => {
let oldEnv
let timezoneSnapshot
beforeEach(() => {
tzInTest.configureAndRestoreLater({
tz: tz(anchorage, 'America/Anchorage'),
tzData: {
'America/Anchorage': anchorage
}
})
oldEnv = window.ENV
window.ENV = {
TIMEZONE: 'America/Anchorage',
FEATURES: {}
}
timezoneSnapshot = timezone.snapshot()
timezone.changeZone(anchorage, 'America/Anchorage')
})
afterEach(async () => {
await flushPromises()
window.ENV = oldEnv
timezone.restore(timezoneSnapshot)
tzInTest.restore()
})
it('preserves the time when shifting to a DST transition day', async () => {

View File

@ -16,7 +16,8 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import timezone from '@canvas/timezone'
import tz from 'timezone'
import timezone, { configure as configureTimezone } from '@canvas/timezone'
import newYork from 'timezone/America/New_York'
import {auditEventStudentAnonymityStates, overallAnonymityStates} from '../AuditTrailHelpers'
@ -42,8 +43,12 @@ describe('AssessmentAuditTray buildAuditTrail()', () => {
const quiz = {id: '123', name: 'Unicorns', role: 'grader'}
beforeEach(() => {
timezoneSnapshot = timezone.snapshot()
timezone.changeZone(newYork, 'America/New_York')
timezoneSnapshot = configureTimezone({
tz: tz(newYork, 'America/New_York'),
tzData: {
'America/New_York': newYork
}
})
auditEvents = []
users = [firstUser, secondUser]
@ -53,7 +58,7 @@ describe('AssessmentAuditTray buildAuditTrail()', () => {
})
afterEach(() => {
timezone.restore(timezoneSnapshot)
configureTimezone(timezoneSnapshot)
})
function buildCreateEvent(payloadValues) {

View File

@ -20,6 +20,18 @@ import mergeI18nTranslations from '../mergeI18nTranslations'
import i18nObj from '../i18nObj'
describe('mergeI18nTranslations', () => {
let originalTranslations
beforeEach(() => {
originalTranslations = i18nObj.translations
i18nObj.translations = {en: {}}
})
afterEach(() => {
i18nObj.translations = originalTranslations
originalTranslations = null
})
it('merges onto i18n.translations', () => {
const newStrings = {
ar: {someKey: 'arabic value'},

View File

@ -126,7 +126,7 @@ describe('Shared > Network > RequestDispatch', () => {
})
it('resolves when flooded with requests', async () => {
const requests = rangeOfLength(4).map(getJSON)
const requests = rangeOfLength(4).map(() => getJSON())
for await (const [index] of requests.entries()) {
// Get the next request
const request = network.getRequests()[index]

View File

@ -0,0 +1,59 @@
/*
* Copyright (C) 2021 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import defaultLocales from 'timezone/locales'
import ar_SA from '../../../ext/custom_timezone_locales/ar_SA'
import ca_ES from '../../../ext/custom_timezone_locales/ca_ES'
import cy_GB from '../../../ext/custom_timezone_locales/cy_GB'
import da_DK from '../../../ext/custom_timezone_locales/da_DK'
import de_DE from '../../../ext/custom_timezone_locales/de_DE'
import el_GR from '../../../ext/custom_timezone_locales/el_GR'
import fa_IR from '../../../ext/custom_timezone_locales/fa_IR'
import fr_CA from '../../../ext/custom_timezone_locales/fr_CA'
import fr_FR from '../../../ext/custom_timezone_locales/fr_FR'
import he_IL from '../../../ext/custom_timezone_locales/he_IL'
import ht_HT from '../../../ext/custom_timezone_locales/ht_HT'
import hy_AM from '../../../ext/custom_timezone_locales/hy_AM'
import is_IS from '../../../ext/custom_timezone_locales/is_IS'
import mi_NZ from '../../../ext/custom_timezone_locales/mi_NZ'
import nn_NO from '../../../ext/custom_timezone_locales/nn_NO'
import pl_PL from '../../../ext/custom_timezone_locales/pl_PL'
import tr_TR from '../../../ext/custom_timezone_locales/tr_TR'
import uk_UA from '../../../ext/custom_timezone_locales/uk_UA'
export default [
...defaultLocales,
ar_SA,
ca_ES,
cy_GB,
da_DK,
de_DE,
el_GR,
fa_IR,
fr_CA,
fr_FR,
he_IL,
ht_HT,
hy_AM,
is_IS,
mi_NZ,
nn_NO,
pl_PL,
tr_TR,
uk_UA,
]

View File

@ -0,0 +1,111 @@
/*
* Copyright (C) 2021 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import tz, { configure } from '../'
import timezone from 'timezone/index'
import french from 'timezone/fr_FR'
import AmericaDenver from 'timezone/America/Denver'
import AmericaChicago from 'timezone/America/Chicago'
import { setup, I18nStubber, moonwalk, epoch, equal } from './helpers'
setup(this)
test('format() should format relative to UTC by default', () =>
equal(tz.format(moonwalk, '%F %T%:z'), '1969-07-21 02:56:00+00:00'))
test('format() should format in en_US by default', () =>
equal(tz.format(moonwalk, '%c'), 'Mon 21 Jul 1969 02:56:00 AM UTC'))
test('format() should parse the value if necessary', () =>
equal(tz.format('1969-07-21 02:56', '%F %T%:z'), '1969-07-21 02:56:00+00:00'))
test('format() should return null if the parse fails', () =>
equal(tz.format('bogus', '%F %T%:z'), null))
test('format() should return null if the format string is unrecognized', () =>
equal(tz.format(moonwalk, 'bogus'), null))
test('format() should preserve 12-hour+am/pm if the locale does define am/pm', () => {
const time = tz.parse('1969-07-21 15:00:00')
equal(tz.format(time, '%-l%P'), '3pm')
equal(tz.format(time, '%I%P'), '03pm')
equal(tz.format(time, '%r'), '03:00:00 PM')
})
test("format() should promote 12-hour+am/pm into 24-hour if the locale doesn't define am/pm", () => {
configure({
tz: timezone(french, 'fr_FR'),
momentLocale: 'fr',
})
const time = tz.parse('1969-07-21 15:00:00')
equal(tz.format(time, '%-l%P'), '15')
equal(tz.format(time, '%I%P'), '15')
equal(tz.format(time, '%r'), '15:00:00')
})
test("format() should use a specific timezone when asked", () => {
configure({
tz: timezone,
tzData: {
'America/Denver': AmericaDenver,
'America/Chicago': AmericaChicago,
}
})
const time = tz.parse('1969-07-21 15:00:00')
equal(tz.format(time, '%-l%P', 'America/Denver'), '9am')
equal(tz.format(time, '%-l%P', 'America/Chicago'), '10am')
})
test('format() should recognize date.formats.*', () => {
I18nStubber.stub('en', {'date.formats.short': '%b %-d'})
equal(tz.format(moonwalk, 'date.formats.short'), 'Jul 21')
})
test('format() should recognize time.formats.*', () => {
I18nStubber.stub('en', {'time.formats.tiny': '%-l:%M%P'})
equal(tz.format(epoch, 'time.formats.tiny'), '12:00am')
})
test('format() should localize when given a localization key', () => {
configure({
tz: timezone('fr_FR', french),
momentLocale: 'fr'
})
I18nStubber.setLocale('fr_FR')
I18nStubber.stub('fr_FR', {'date.formats.full': '%-d %b %Y %-l:%M%P'})
equal(tz.format(moonwalk, 'date.formats.full'), '21 juil. 1969 2:56')
})
test('format() should automatically convert %l to %-l when given a localization key', () => {
I18nStubber.stub('en', {'time.formats.tiny': '%l:%M%P'})
equal(tz.format(moonwalk, 'time.formats.tiny'), '2:56am')
})
test('format() should automatically convert %k to %-k when given a localization key', () => {
I18nStubber.stub('en', {'time.formats.tiny': '%k:%M'})
equal(tz.format(moonwalk, 'time.formats.tiny'), '2:56')
})
test('format() should automatically convert %e to %-e when given a localization key', () => {
I18nStubber.stub('en', {'date.formats.short': '%b %e'})
equal(tz.format(epoch, 'date.formats.short'), 'Jan 1')
})

View File

@ -0,0 +1,40 @@
/*
* Copyright (C) 2021 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'translations/_core_en'
import I18nStubber from '../../../../spec/coffeescripts/helpers/I18nStubber'
import tz from 'timezone/index'
import { configure } from '../'
export function setup() {
beforeEach(() => {
configure({ tz, tzData: {} })
I18nStubber.pushFrame()
})
afterEach(() => {
I18nStubber.popFrame()
configure({})
})
}
export const moonwalk = new Date(Date.UTC(1969, 6, 21, 2, 56))
export const epoch = new Date(Date.UTC(1970, 0, 1, 0, 0))
export const equal = (a, b) => expect(a).toEqual(b)
export const ok = a => expect(a).toBeTruthy()
export { I18nStubber }

View File

@ -0,0 +1,305 @@
/*
* Copyright (C) 2021 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import tz, { configure } from '../'
import timezone from 'timezone'
import french from 'timezone/fr_FR'
import chinese from 'timezone/zh_CN'
import en_US from 'timezone/en_US'
import MockDate from 'mockdate'
import { I18nStubber, equal, epoch, ok, moonwalk } from './helpers'
import {
up as configureDateTimeMomentParser,
down as resetDateTimeMomentParser
} from '../../../../ui/boot/initializers/configureDateTimeMomentParser'
describe('english tz', () => {
beforeAll(() => {
MockDate.set('2015-02-01', 'UTC')
I18nStubber.pushFrame()
I18nStubber.setLocale('en_US')
I18nStubber.stub('en_US', {
'date.formats.date_at_time': '%b %-d at %l:%M%P',
'date.formats.default': '%Y-%m-%d',
'date.formats.full': '%b %-d, %Y %-l:%M%P',
'date.formats.full_with_weekday': '%a %b %-d, %Y %-l:%M%P',
'date.formats.long': '%B %-d, %Y',
'date.formats.long_with_weekday': '%A, %B %-d',
'date.formats.medium': '%b %-d, %Y',
'date.formats.medium_month': '%b %Y',
'date.formats.medium_with_weekday': '%a %b %-d, %Y',
'date.formats.short': '%b %-d',
'date.formats.short_month': '%b',
'date.formats.short_weekday': '%a',
'date.formats.short_with_weekday': '%a, %b %-d',
'date.formats.weekday': '%A',
'time.formats.default': '%a, %d %b %Y %H:%M:%S %z',
'time.formats.long': '%B %d, %Y %H:%M',
'time.formats.short': '%d %b %H:%M',
'time.formats.tiny': '%l:%M%P',
'time.formats.tiny_on_the_hour': '%l%P'
})
configure({
tz: timezone('en_US', en_US),
momentLocale: 'en',
})
configureDateTimeMomentParser()
})
afterAll(() => {
resetDateTimeMomentParser()
MockDate.reset()
I18nStubber.clear()
})
test('parses english dates', () => {
const engDates = [
'08/03/2015',
'8/3/2015',
'August 3, 2015',
'Aug 3, 2015',
'3 Aug 2015',
'2015-08-03',
'2015 08 03',
'August 3, 2015',
'Monday, August 3',
'Mon Aug 3, 2015',
'Mon, Aug 3',
'Aug 3'
]
engDates.forEach(date => {
const d = tz.parse(date)
equal(tz.format(d, '%d'), '03', `this works: ${date}`)
})
})
test('parses english times', () => {
const engTimes = ['6:06 PM', '6:06:22 PM', '6:06pm', '6pm']
engTimes.forEach(time => {
const d = tz.parse(time)
equal(tz.format(d, '%H'), '18', `this works: ${time}`)
})
})
test('parses english date times', () => {
const engDateTimes = [
'2015-08-03 18:06:22',
'August 3, 2015 6:06 PM',
'Aug 3, 2015 6:06 PM',
'Aug 3, 2015 6pm',
'Monday, August 3, 2015 6:06 PM',
'Mon, Aug 3, 2015 6:06 PM',
'Aug 3 at 6:06pm',
'Aug 3, 2015 6:06pm',
'Mon Aug 3, 2015 6:06pm'
]
engDateTimes.forEach(dateTime => {
const d = tz.parse(dateTime)
equal(tz.format(d, '%d %H'), '03 18', `this works: ${dateTime}`)
})
})
test('parses 24hr times even if the locale lacks them', () => {
const d = tz.parse('18:06')
equal(tz.format(d, '%H:%M'), '18:06')
})
})
describe('french tz', () => {
beforeAll(() => {
MockDate.set('2015-02-01', 'UTC')
I18nStubber.pushFrame()
I18nStubber.setLocale('fr_FR')
I18nStubber.stub('fr_FR', {
'date.formats.date_at_time': '%-d %b à %k:%M',
'date.formats.default': '%d/%m/%Y',
'date.formats.full': '%b %-d, %Y %-k:%M',
'date.formats.full_with_weekday': '%a %-d %b, %Y %-k:%M',
'date.formats.long': 'le %-d %B %Y',
'date.formats.long_with_weekday': '%A, %-d %B',
'date.formats.medium': '%-d %b %Y',
'date.formats.medium_month': '%b %Y',
'date.formats.medium_with_weekday': '%a %-d %b %Y',
'date.formats.short': '%-d %b',
'date.formats.short_month': '%b',
'date.formats.short_weekday': '%a',
'date.formats.short_with_weekday': '%a, %-d %b',
'date.formats.weekday': '%A',
'time.formats.default': '%a, %d %b %Y %H:%M:%S %z',
'time.formats.long': ' %d %B, %Y %H:%M',
'time.formats.short': '%d %b %H:%M',
'time.formats.tiny': '%k:%M',
'time.formats.tiny_on_the_hour': '%k:%M'
})
configure({
tz: timezone('fr_FR', french),
momentLocale: 'fr'
})
configureDateTimeMomentParser()
})
afterAll(() => {
resetDateTimeMomentParser()
MockDate.reset()
I18nStubber.clear()
})
const frenchDates = [
'03/08/2015',
'3/8/2015',
'3 août 2015',
'2015-08-03',
'le 3 août 2015',
'lundi, 3 août',
'lun. 3 août 2015',
'3 août',
'lun., 3 août',
'3 août 2015',
'3 août'
]
for (const date of frenchDates) {
test(`parses french date "${date}"`, () => {
equal(tz.format(tz.parse(date), '%d'), '03')
})
}
const frenchTimes = ['18:06', '18:06:22']
for (const time of frenchTimes) {
test(`parses french time "${time}"`, () => {
equal(tz.format(tz.parse(time), '%H'), '18')
})
}
const frenchDateTimes = [
'2015-08-03 18:06:22',
'3 août 2015 18:06',
'lundi 3 août 2015 18:06',
'lun. 3 août 2015 18:06',
'3 août à 18:06',
'août 3, 2015 18:06',
'lun. 3 août, 2015 18:06'
]
for (const dateTime of frenchDateTimes) {
test(`parses french date time "${dateTime}"`, () => {
equal(tz.format(tz.parse(dateTime), '%d %H'), '03 18')
})
}
})
describe('chinese tz', () => {
beforeAll(() => {
MockDate.set('2015-02-01', 'UTC')
I18nStubber.pushFrame()
I18nStubber.setLocale('zh_CN')
I18nStubber.stub('zh_CN', {
'date.formats.date_at_time': '%b %-d 于 %H:%M',
'date.formats.default': '%Y-%m-%d',
'date.formats.full': '%b %-d, %Y %-l:%M%P',
'date.formats.full_with_weekday': '%a %b %-d, %Y %-l:%M%P',
'date.formats.long': '%Y %B %-d',
'date.formats.long_with_weekday': '%A, %B %-d',
'date.formats.medium': '%Y %b %-d',
'date.formats.medium_month': '%Y %b',
'date.formats.medium_with_weekday': '%a %Y %b %-d',
'date.formats.short': '%b %-d',
'date.formats.short_month': '%b',
'date.formats.short_weekday': '%a',
'date.formats.short_with_weekday': '%a, %b %-d',
'date.formats.weekday': '%A',
'time.formats.default': '%a, %Y %b %d %H:%M:%S %z',
'time.formats.long': '%Y %B %d %H:%M',
'time.formats.short': '%b %d %H:%M',
'time.formats.tiny': '%H:%M',
'time.formats.tiny_on_the_hour': '%k:%M'
})
configure({
tz: timezone(chinese, 'zh_CN'),
momentLocale: 'zh-cn'
})
configureDateTimeMomentParser()
})
afterAll(() => {
resetDateTimeMomentParser()
MockDate.reset()
I18nStubber.clear()
})
test('parses chinese dates', () => {
const chineseDates = [
'2015-08-03',
'2015年8月3日',
'2015 八月 3',
'2015 8月 3',
'星期一, 八月 3',
'一 2015 8月 3',
'一, 8月 3',
'8月 3'
]
chineseDates.forEach(date => {
expect(tz.format(tz.parse(date), '%d')).toEqual('03')
})
})
test('parses chinese date AM times', () => {
const chineseDateTimes = [
'2015-08-03 06:06:22',
'2015年8月3日6点06分',
'2015年8月3日星期一6点06分',
'8月 3日 于 6:06',
'20158月3日, 6:06',
'一 20158月3日, 6:06' // this is incorrectly parsing as "Fri, 20 Mar 1908 06:06:00 GMT"
]
chineseDateTimes.forEach(dateTime => {
equal(tz.format(tz.parse(dateTime), '%d %H'), '03 06')
})
})
test('parses chinese date PM times', () => {
const chineseDateTimes = [
'2015-08-03 18:06:22',
'2015年8月3日晚上6点06分',
// '2015年8月3日星期一晚上6点06分', // parsing as "Mon, 03 Aug 2015 06:06:00 GMT"
'8月 3日, 于 18:06',
// '2015 8月 3日, 6:06下午', // doesn't recognize 下午 as implying PM
// '一 2015 8月 3日, 6:06下午'
]
chineseDateTimes.forEach(dateTime => {
equal(tz.format(tz.parse(dateTime), '%d %H'), '03 18')
})
})
})

View File

@ -0,0 +1,161 @@
/*
* Copyright (C) 2021 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
//
// this is only meant for use by maintainers and is not actually exercised in
// the build mainly because the locale files aren't available to the runner
//
// use this only if you're doing something substantial to tz/moment/i18n
//
import 'translations/_core'
import 'translations/_core_en'
import tz, { configure } from '../'
import timezone from 'timezone'
import $ from '@canvas/datetime'
import I18n from '@canvas/i18n'
import {
up as configureDateTimeMomentParser,
down as resetDateTimeMomentParser
} from '../../../boot/initializers/configureDateTimeMomentParser'
import tzLocales from './bigeasyLocales'
import fs from 'fs'
import path from 'path'
import YAML from 'yaml'
import '../../../ext/custom_moment_locales/ca'
import '../../../ext/custom_moment_locales/de'
import '../../../ext/custom_moment_locales/he'
import '../../../ext/custom_moment_locales/pl'
import '../../../ext/custom_moment_locales/fa'
import '../../../ext/custom_moment_locales/fr'
import '../../../ext/custom_moment_locales/fr_ca'
import '../../../ext/custom_moment_locales/ht_ht'
import '../../../ext/custom_moment_locales/mi_nz'
import '../../../ext/custom_moment_locales/hy_am'
import '../../../ext/custom_moment_locales/sl'
const locales = loadAvailableLocales()
const tzLocaleData = tzLocales.reduce((acc, locale) => {
acc[locale.name] = locale
return acc
}, {})
describe('english tz', () => {
const dates = createDateSamples()
beforeEach(configureDateTimeMomentParser)
afterEach(resetDateTimeMomentParser)
for (const locale of locales) {
test(`timezone -> moment for ${locale.key}`, () => {
I18n.locale = locale.key
configure({
tz: timezone(locale.bigeasy, tzLocaleData[locale.bigeasy]),
momentLocale: locale.moment
})
for (const date of dates) {
const formattedDate = $.dateString(date)
const formattedTime = tz.format(date, 'time.formats.tiny')
const formatted = `${formattedDate} ${formattedTime}`
expect(
tz.parse(formatted).getTime()
).toEqual(
date.getTime()
)
}
})
test(`hour format matches timezone locale for ${locale.key}`, () => {
if (locale.key === 'ca') {
pending("it's broken for ca, needs investigation")
}
I18n.locale = locale.key
configure({
tz: timezone(locale.bigeasy, tzLocaleData[locale.bigeasy]),
momentLocale: locale.moment
})
const formats = [
'date.formats.date_at_time',
'date.formats.full',
'date.formats.full_with_weekday',
'time.formats.tiny',
'time.formats.tiny_on_the_hour'
]
for (const format of formats) {
// expect(tz.hasMeridian()).toEqual(/%p/i.test(I18n.lookup(format)))
expect(tz.hasMeridian() || !/%p/i.test(I18n.lookup(format))).toBeTruthy()
}
// const invalid = key => {
// const format = I18n.lookup(key)
// // ok(/%p/i.test(format) === tz.hasMeridian(), `format: ${format}, hasMeridian: ${tz.hasMeridian()}`)
// ok(
// tz.hasMeridian() || !/%p/i.test(format),
// `format: ${format}, hasMeridian: ${tz.hasMeridian()}`
// )
// }
// ok(!formats.forEach(invalid))
})
}
})
function createDateSamples() {
const dates = []
const currentYear = (new Date()).getFullYear()
const otherYear = currentYear + 4
for (let i = 0; i < 12; ++i) {
dates.push(new Date(Date.UTC(currentYear, i, 1, 23, 59)))
dates.push(new Date(Date.UTC(currentYear, i, 28, 23, 59)))
dates.push(new Date(Date.UTC(otherYear, i, 7, 23, 59)))
dates.push(new Date(Date.UTC(otherYear, i, 15, 23, 59)))
}
return dates
}
function loadAvailableLocales() {
const manifest = (
YAML.parse(
fs.readFileSync(
path.resolve(__dirname, '../../../../config/locales/locales.yml'),
'utf8'
)
)
)
return Object.keys(manifest).map(key => {
const locale = manifest[key]
const base = key.split('-')[0]
return {
key,
moment: locale.moment_locale || key.toLowerCase(),
bigeasy: locale.bigeasy_locale || manifest[base].bigeasy_locale
}
}).filter(x => x.key)
}

View File

@ -0,0 +1,25 @@
/*
* Copyright (C) 2021 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import tz from '../'
import { equal, moonwalk, epoch, setup } from './helpers'
setup(this)
test('mergeTimeAndDate() finds the given time of day on the given date.', () =>
equal(+tz.mergeTimeAndDate(moonwalk, epoch), +new Date(Date.UTC(1970, 0, 1, 2, 56))))

View File

@ -0,0 +1,102 @@
/*
* Copyright (C) 2021 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import tz, { configure } from '../'
import timezone from 'timezone'
import detroit from 'timezone/America/Detroit'
import french from 'timezone/fr_FR'
import { setup, I18nStubber, equal, epoch, ok, moonwalk, preload } from './helpers'
setup(this)
test('hasMeridian() true if locale defines am/pm', () => ok(tz.hasMeridian()))
test("hasMeridian() false if locale doesn't define am/pm", () => {
configure({
tz: timezone('fr_FR', french),
momentLocale: 'fr'
})
ok(!tz.hasMeridian())
})
test('useMeridian() true if locale defines am/pm and uses 12-hour format', () => {
I18nStubber.stub('en', {'time.formats.tiny': '%l:%M%P'})
ok(tz.hasMeridian())
ok(tz.useMeridian())
})
test('useMeridian() false if locale defines am/pm but uses 24-hour format', () => {
I18nStubber.stub('en', {'time.formats.tiny': '%k:%M'})
ok(tz.hasMeridian())
ok(!tz.useMeridian())
})
test("useMeridian() false if locale doesn't define am/pm and instead uses 24-hour format", () => {
configure({
tz: timezone(french, 'fr_FR'),
momentLocale: 'fr'
})
I18nStubber.setLocale('fr_FR')
I18nStubber.stub('fr_FR', {'time.formats.tiny': '%-k:%M'})
ok(!tz.hasMeridian())
ok(!tz.useMeridian())
})
test("useMeridian() false if locale doesn't define am/pm but still uses 12-hour format (format will be corrected)", () => {
configure({
tz: timezone(french, 'fr_FR'),
momentLocale: 'fr'
})
I18nStubber.setLocale('fr_FR')
I18nStubber.stub('fr_FR', {'time.formats.tiny': '%-l:%M%P'})
ok(!tz.hasMeridian())
ok(!tz.useMeridian())
})
test('isMidnight() is false when no argument given.', () => ok(!tz.isMidnight()))
test('isMidnight() is false when invalid date is given.', () => {
const date = new Date('invalid date')
ok(!tz.isMidnight(date))
})
test('isMidnight() is true when date given is at midnight.', () => ok(tz.isMidnight(epoch)))
test("isMidnight() is false when date given isn't at midnight.", () => ok(!tz.isMidnight(moonwalk)))
test('isMidnight() is false when date is midnight in a different zone.', () => {
configure({ tz: timezone(detroit, 'America/Detroit') })
ok(!tz.isMidnight(epoch))
})
test('changeToTheSecondBeforeMidnight() returns null when no argument given.', () =>
equal(tz.changeToTheSecondBeforeMidnight(), null))
test('changeToTheSecondBeforeMidnight() returns null when invalid date is given.', () => {
const date = new Date('invalid date')
equal(tz.changeToTheSecondBeforeMidnight(date), null)
})
test('changeToTheSecondBeforeMidnight() returns fancy midnight when a valid date is given.', () => {
const fancyMidnight = tz.changeToTheSecondBeforeMidnight(epoch)
equal(fancyMidnight.toGMTString(), 'Thu, 01 Jan 1970 23:59:59 GMT')
})

View File

@ -0,0 +1,55 @@
/*
* Copyright (C) 2021 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import tz, { configure } from '../'
import timezone from 'timezone'
import { moonwalk, epoch, equal, setup } from './helpers'
setup(this)
test('parse(valid datetime string)', () => {
equal(+tz.parse(moonwalk.toISOString()), +moonwalk)
})
test('parse(timestamp integer)', () => {
equal(+tz.parse(+moonwalk), +moonwalk)
})
test('parse(Date object)', () => {
equal(+tz.parse(moonwalk), +moonwalk)
})
test('parse(date array)', () => {
equal(+tz.parse([1969, 7, 21, 2, 56]), +moonwalk)
})
test('parse() should return null on failure', () => equal(tz.parse('bogus'), null))
test('parse() should return a date on success', () => equal(typeof tz.parse(+moonwalk), 'object'))
test('parse("") should fail', () => equal(tz.parse(''), null))
test('parse(null) should fail', () => equal(tz.parse(null), null))
test('parse(integer) should be ms since epoch', () => equal(+tz.parse(2016), +timezone(2016)))
test('parse("looks like integer") should be a year', () =>
equal(+tz.parse('2016'), +tz.parse('2016-01-01')))
test('parse() should parse relative to UTC by default', () =>
equal(+tz.parse('1969-07-21 02:56'), +moonwalk))

View File

@ -0,0 +1,37 @@
/*
* Copyright (C) 2021 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import tz from '../'
import { equal, moonwalk, setup } from './helpers'
setup(this)
test('shift() should adjust the date as appropriate', () =>
equal(+tz.shift(moonwalk, '-1 day'), moonwalk - 86400000))
test('shift() should apply multiple directives', () =>
equal(+tz.shift(moonwalk, '-1 day', '-1 hour'), moonwalk - 86400000 - 3600000))
test('shift() should parse the value if necessary', () =>
equal(+tz.shift('1969-07-21 02:56', '-1 day'), moonwalk - 86400000))
test('shift() should return null if the parse fails', () =>
equal(tz.shift('bogus', '-1 day'), null))
test('shift() should return null if the directives includes a format string', () =>
equal(tz.shift('bogus', '-1 day', '%F %T%:z'), null))

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2013 - present Instructure, Inc.
* Copyright (C) 2021 - present Instructure, Inc.
*
* This file is part of Canvas.
*
@ -16,338 +16,244 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import _ from 'underscore'
import __tz from 'timezone/index'
import I18n from '@canvas/i18n'
import moment from 'moment'
import MomentFormats from './moment_formats'
import timezone from 'timezone'
import en_US from 'timezone/en_US'
import parseDateTimeWithMoment, {
specifiesTimezone,
toRFC3339WithoutTZ,
} from 'date-time-moment-parser'
// start with the bare vendor-provided tz() function
let _tz = __tz
let currentLocale = 'en_US' // default to US locale
let momentLocale = 'en'
const _preloadedData =
window.__PRELOADED_TIMEZONE_DATA__ || (window.__PRELOADED_TIMEZONE_DATA__ = {})
const initialState = Object.freeze({
// a timezone instance configured for the user's current timezone and locale
//
// import timezone from 'timezone'
// import Denver from 'timezone/America/Denver'
// import french from 'timezone/fr_FR'
//
// const tz = timezone('America/Denver', Denver, 'fr_FR', french)
//
// tz: timezone(en_US, 'en_US', 'UTC'),
tz: timezone(en_US, 'en_US', 'UTC'),
// wrap it up in a set of methods that will always call the most up-to-date
// version. each method is intended to act as a subset of bigeasy's generic
// tz() functionality.
const tz = {
_preloadedData,
// a mapping of timezones (e.g. America/Denver) to their timezone data (e.g.
// import Denver from "timezone/America/Denver") that is used when formatting
// to a timezone different than the default
tzData: {},
// wrap's moment() for parsing datetime strings. assumes the string to be
// parsed is in the profile timezone unless if contains an offset string
// *and* a format token to parse it, and unfudges the result.
moment(input, format) {
// ensure first argument is a string and second is a format or an array
// of formats
if (!_.isString(input) || !(_.isString(format) || _.isArray(format))) {
throw new Error(
'tz.moment only works on string+format(s). just use moment() directly for any other signature'
)
}
// used for parsing datetimes when timezone is unable to parse a value
momentLocale: 'en',
})
// call out to moment, leaving the result alone if invalid
let m = moment.apply(null, [input, format, momentLocale])
if (m._pf.unusedTokens.length > 0) {
// we didn't use strict at first, because we want to accept when
// there's unused input as long as we're using all tokens. but if the
// best non-strict match has unused tokens, reparse with strict
m = moment.apply(null, [input, format, momentLocale, true])
}
if (!m.isValid()) return m
const state = Object.assign({}, initialState)
// unfudge the result unless an offset was both specified and used in the
// parsed string.
//
// using moment internals here because I can't come up with any better
// reliable way to test for this :( fortunately, both _f and
// _pf.unusedTokens are always set as long as format is explicitly
// specified as either a string or array (which we've already checked
// for).
//
// _f lacking a 'Z' indicates that no offset token was specified in the
// format string used in parsing. we check this instead of just format in
// case format is an array, of which one contains a Z and the other
// doesn't, and we don't know until after parsing which format would best
// match the input.
//
// _pf.unusedTokens having a 'Z' token indicates that even though the
// format used contained a 'Z' token (since the first condition wasn't
// false), that token was not used during parsing; i.e. the input string
// didn't provide a value for it.
//
if (!m._f.match(/Z/) || m._pf.unusedTokens.indexOf('Z') >= 0) {
const l = m.locale()
m = moment(tz.raw_parse(m.locale('en').format('YYYY-MM-DD HH:mm:ss')))
m.locale(l)
}
export function configure({ tz, tzData, momentLocale }) {
const previousState = Object.assign({}, state)
return m
},
state.tz = tz || initialState.tz
state.tzData = tzData || initialState.tzData
state.momentLocale = momentLocale || initialState.momentLocale
// interprets a date value (string, integer, Date, date array, etc. -- see
// bigeasy's tz() docs) according to _tz. returns null on parse failure.
// otherwise returns a Date (rather than _tz()'s timestamp integer)
// because, when treated correctly, they are interchangeable but the Date
// is more convenient.
raw_parse(value) {
const timestamp = _tz(value)
if (typeof timestamp === 'number') {
return new Date(timestamp)
}
return null
},
// parses a date value but more robustly. returns null on parse failure. if
// the value is a string but does not look like an ISO8601 string
// (loosely), or otherwise fails to be interpreted by raw_parse(), then
// parsing will be attempted with tz.moment() according to the formats
// defined in MomentFormats.getFormats(). also note that raw_parse('') will
// return the epoch, but parse('') will return null.
parse(value) {
// hard code '' and null as unparseable
if (value === '' || value == null) return null
if (!_.isString(value)) {
// try and understand the value through _tz. if it doesn't work, we
// don't know what else to do with it as a non-string
return tz.raw_parse(value)
}
// only try _tz with strings looking loosely like an ISO8601 value. in
// particular, we want to avoid parsing e.g. '2016' as 2,016 milliseconds
// since the epoch
if (value.match(/[-:]/)) {
const result = tz.raw_parse(value)
if (result) return result
}
// _tz parsing failed or skipped. try moment parsing
const formats = MomentFormats.getFormats()
const m = tz.moment(value, formats)
return m.isValid() ? m.toDate() : null
},
// format a date value (parsing it if necessary). returns null for parse
// failure on the value or an unrecognized format string.
format(value, format, otherZone) {
let localTz = _tz
const usingOtherZone = arguments.length === 3 && otherZone
if (usingOtherZone) {
if (!(otherZone in _preloadedData)) return null
localTz = _tz(_preloadedData[otherZone])
}
// make sure we have a good value first
const datetime = tz.parse(value)
if (datetime == null) return null
format = tz.adjustFormat(format)
// try and apply the format string to the datetime. if it succeeds, we'll
// get a string; otherwise we'll get the (non-string) date back.
let formatted = null
if (usingOtherZone) {
formatted = localTz(datetime, format, otherZone)
} else {
formatted = localTz(datetime, format)
}
if (typeof formatted !== 'string') return null
return formatted
},
adjustFormat(format) {
// translate recognized 'date.formats.*' and 'time.formats.*' to
// appropriate format strings according to locale.
if (format.match(/^(date|time)\.formats\./)) {
const locale_format = I18n.lookup(format)
if (locale_format) {
// in the process, turn %l, %k, and %e into %-l, %-k, and %-e
// (respectively) to avoid extra unnecessary space characters
//
// javascript doesn't have lookbehind, so do the fixing on the reversed
// string so we can use lookahead instead. the funky '(%%)*(?!%)' pattern
// in all the regexes is to make sure we match (once unreversed), e.g.,
// both %l and %%%l (literal-% + %l) but not %%l (literal-% + l).
format = locale_format
.split('')
.reverse()
.join('')
.replace(/([lke])(?=%(%%)*(?!%))/, '$1-')
.split('')
.reverse()
.join('')
}
}
// some locales may not (according to bigeasy's localization files) use
// an am/pm distinction, but could then be incorrectly used with 12-hour
// format strings (e.g. %l:%M%P), whether by erroneous format strings in
// canvas' localization files or by unlocalized format strings. as a
// result, you might get 3am and 3pm both formatting to the same value.
// to prevent this, 12-hour indicators with an am/pm indicator should be
// promoted to the equivalent 24-hour indicator when the locale defines
// %P as an empty string. ("reverse, look-ahead, reverse" pattern for
// same reason as above)
format = format
.split('')
.reverse()
.join('')
if (
!tz.hasMeridian() &&
((format.match(/[lI][-_]?%(%%)*(?!%)/) && format.match(/p%(%%)*(?!%)/i)) ||
format.match(/r[-_]?%(%%)*(?!%)/))
) {
format = format.replace(/l(?=[-_]?%(%%)*(?!%))/, 'k')
format = format.replace(/I(?=[-_]?%(%%)*(?!%))/, 'H')
format = format.replace(/r(?=[-_]?%(%%)*(?!%))/, 'T')
}
format = format
.split('')
.reverse()
.join('')
return format
},
hasMeridian() {
return _tz(new Date(), '%P') !== ''
},
useMeridian() {
if (!this.hasMeridian()) return false
const tiny = I18n.lookup('time.formats.tiny')
return tiny && tiny.match(/%-?l/)
},
// apply any number of non-format directives to the value (parsing it if
// necessary). return null for parse failure on the value or if one of the
// directives was mistakenly a format string. returns the modified Date
// otherwise. typical directives will be for date math, e.g. '-3 days'.
// non-format unrecognized directives are ignored.
shift(value) {
// make sure we have a good value first
const datetime = tz.parse(value)
if (datetime == null) return null
// no application strings given? just regurgitate the input (though
// parsed now).
if (arguments.length == 1) return datetime
// try and apply the directives to the datetime. if one was a format
// string (unacceptable) we'll get a (non-integer) string back.
// otherwise, we'll get a new timestamp integer back (even if some
// unrecognized non-format applications were ignored).
const args = [datetime].concat([].slice.apply(arguments, [1]))
const timestamp = _tz(...args)
if (typeof timestamp !== 'number') return null
return new Date(timestamp)
},
// allow snapshotting and restoration, and extending through the
// vendor-provided tz()'s functional composition
snapshot() {
return [_tz, currentLocale, momentLocale]
},
restore(snapshot) {
// we can't actually check that the snapshot has appropriate values, but
// we can at least verify the shape of [function, string, string]
if (!_.isArray(snapshot)) throw new Error('invalid tz() snapshot')
if (typeof snapshot[0] !== 'function') throw new Error('invalid tz() snapshot')
if (!_.isString(snapshot[1])) throw new Error('invalid tz() snapshot')
if (!_.isString(snapshot[2])) throw new Error('invalid tz() snapshot')
_tz = snapshot[0]
currentLocale = snapshot[1]
momentLocale = snapshot[2]
},
extendConfiguration() {
const extended = _tz(...arguments)
if (typeof extended !== 'function') throw new Error('invalid tz() extension')
_tz = extended
},
// apply a "feature" to tz (NOTE: persistent and shared). the provided
// feature can be a chunk of previously loaded data, which is applied
// immediately, or the name of a data file to load and then apply
// asynchronously.
applyFeature(data, name) {
function extendConfig(preloadedData) {
tz.extendConfiguration(preloadedData, name)
return Promise.resolve()
}
if (arguments.length > 1) {
this.preload(name, data)
return extendConfig(data)
}
name = data
const preloadedData = this.preload(name)
if (preloadedData instanceof Promise) {
return preloadedData.then(extendConfig)
}
return extendConfig(preloadedData)
},
// preload a specific data file without having to actually
// change the timezone to do it. Future "applyFeature" calls
// will apply synchronously if their data is already preloaded.
preload(name, data) {
if (arguments.length > 1) {
_preloadedData[name] = data
return _preloadedData[name]
} else if (_preloadedData[name]) {
return _preloadedData[name]
} else {
return new Promise((resolve, reject) =>
reject(
new Error(
`In webpack, loading timezones on-demand is not supported. ${name}" should already be script-tagged onto the page from Rails.`
)
)
)
}
},
changeLocale() {
if (arguments.length > 2) {
currentLocale = arguments[1]
momentLocale = arguments[2]
} else {
currentLocale = arguments[0]
momentLocale = arguments[1]
}
// take off the momentLocale before passing up the chain
const args = [].slice.apply(arguments).slice(0, arguments.length - 1)
return this.applyFeature.apply(this, args)
},
isMidnight(date) {
if (date == null) {
return false
}
return tz.format(date, '%R') === '00:00'
},
changeToTheSecondBeforeMidnight(date) {
return tz.parse(tz.format(date, '%F 23:59:59'))
},
setToEndOfMinute(date) {
return tz.parse(tz.format(date, '%F %R:59'))
},
// finds the given time of day on the given date ignoring dst conversion and such.
// e.g. if time is 2016-05-20 14:00:00 and date is 2016-03-17 23:59:59, the result will
// be 2016-03-17 14:00:00
mergeTimeAndDate(time, date) {
return tz.parse(tz.format(date, '%F ') + tz.format(time, '%T'))
}
return previousState
}
// changing zone and locale are just aliases for applying a feature
tz.changeZone = tz.applyFeature
export function parse(value) {
const { tz, momentLocale } = state
export default tz
// hard code '' and null as unparseable
if (value === '' || value === null || value === undefined) {
return null
}
// we don't want to use tz for parsing any string that doesn't look like a
// datetime string
if (typeof value !== 'string' || value.match(/[-:]/)) {
const epoch = tz(value)
if (typeof epoch === 'number') {
return new Date(epoch)
}
}
// try with moment
if (typeof value === 'string') {
let m = parseDateTimeWithMoment(value, momentLocale)
if (m && !specifiesTimezone(m)) {
const fudged = tz(toRFC3339WithoutTZ(m))
m = moment(new Date(fudged))
m.locale(momentLocale)
}
if (m) {
return m.toDate()
}
}
return null
}
// format a date value (parsing it if necessary). returns null for parse
// failure on the value or an unrecognized format string.
export function format(value, inputFormat, otherZone) {
const { tzData } = state
const usingOtherZone = arguments.length === 3 && otherZone
let tz = state.tz
if (usingOtherZone) {
if (!(otherZone in tzData)) {
console.warn(
`You are asking to format DateTime into a timezone that is not supported -- ${otherZone}`
)
return null
}
tz = tz(tzData[otherZone])
}
// make sure we have a good value first
const datetime = parse(value)
if (datetime === null) return null
const localizedFormat = adjustFormat(inputFormat)
// try and apply the format string to the datetime. if it succeeds, we'll
// get a string; otherwise we'll get the (non-string) date back.
let formatted = null
if (usingOtherZone) {
formatted = tz(datetime, localizedFormat, otherZone)
} else {
formatted = tz(datetime, localizedFormat)
}
if (typeof formatted !== 'string') return null
return formatted
}
export function adjustFormat(format) {
// translate recognized 'date.formats.*' and 'time.formats.*' to
// appropriate format strings according to locale.
if (format.match(/^(date|time)\.formats\./)) {
const localeFormat = I18n.lookup(format)
if (localeFormat) {
// in the process, turn %l, %k, and %e into %-l, %-k, and %-e
// (respectively) to avoid extra unnecessary space characters
//
// javascript doesn't have lookbehind, so do the fixing on the reversed
// string so we can use lookahead instead. the funky '(%%)*(?!%)' pattern
// in all the regexes is to make sure we match (once unreversed), e.g.,
// both %l and %%%l (literal-% + %l) but not %%l (literal-% + l).
format = localeFormat
.split('')
.reverse()
.join('')
.replace(/([lke])(?=%(%%)*(?!%))/, '$1-')
.split('')
.reverse()
.join('')
}
}
// some locales may not (according to bigeasy's localization files) use
// an am/pm distinction, but could then be incorrectly used with 12-hour
// format strings (e.g. %l:%M%P), whether by erroneous format strings in
// canvas' localization files or by unlocalized format strings. as a
// result, you might get 3am and 3pm both formatting to the same value.
// to prevent this, 12-hour indicators with an am/pm indicator should be
// promoted to the equivalent 24-hour indicator when the locale defines
// %P as an empty string. ("reverse, look-ahead, reverse" pattern for
// same reason as above)
format = format
.split('')
.reverse()
.join('')
if (
!hasMeridian() &&
((format.match(/[lI][-_]?%(%%)*(?!%)/) && format.match(/p%(%%)*(?!%)/i)) ||
format.match(/r[-_]?%(%%)*(?!%)/))
) {
format = format.replace(/l(?=[-_]?%(%%)*(?!%))/, 'k')
format = format.replace(/I(?=[-_]?%(%%)*(?!%))/, 'H')
format = format.replace(/r(?=[-_]?%(%%)*(?!%))/, 'T')
}
format = format
.split('')
.reverse()
.join('')
return format
}
export function hasMeridian() {
const { tz } = state
return tz(new Date(), '%P') !== ''
}
export function useMeridian() {
if (!hasMeridian()) return false
const tiny = I18n.lookup('time.formats.tiny')
return tiny && tiny.match(/%-?l/)
}
// apply any number of non-format directives to the value (parsing it if
// necessary). return null for parse failure on the value or if one of the
// directives was mistakenly a format string. returns the modified Date
// otherwise. typical directives will be for date math, e.g. '-3 days'.
// non-format unrecognized directives are ignored.
export function shift(value) {
const { tz } = state
// make sure we have a good value first
const datetime = parse(value)
if (datetime === null) return null
// no application strings given? just regurgitate the input (though
// parsed now).
if (arguments.length === 1) return datetime
// try and apply the directives to the datetime. if one was a format
// string (unacceptable) we'll get a (non-integer) string back.
// otherwise, we'll get a new timestamp integer back (even if some
// unrecognized non-format applications were ignored).
const args = [datetime].concat([].slice.apply(arguments, [1]))
const timestamp = tz(...args)
if (typeof timestamp !== 'number') return null
return new Date(timestamp)
}
export function isMidnight(date) {
if (date === null) {
return false
}
return format(date, '%R') === '00:00'
}
export function changeToTheSecondBeforeMidnight(date) {
return parse(format(date, '%F 23:59:59'))
}
export function setToEndOfMinute(date) {
return parse(format(date, '%F %R:59'))
}
// finds the given time of day on the given date ignoring dst conversion and such.
// e.g. if time is 2016-05-20 14:00:00 and date is 2016-03-17 23:59:59, the result will
// be 2016-03-17 14:00:00
export function mergeTimeAndDate(time, date) {
return parse(format(date, '%F ') + format(time, '%T'))
}
export default {
adjustFormat,
changeToTheSecondBeforeMidnight,
format,
hasMeridian,
isMidnight,
mergeTimeAndDate,
parse,
setToEndOfMinute,
shift,
useMeridian,
}

View File

@ -1,168 +0,0 @@
/*
* Copyright (C) 2015 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import _ from 'underscore'
import I18n from 'i18n!instructure'
import moment from 'moment'
function eventTimes(dateFormats, timeFormats) {
const formats = []
dateFormats.forEach(df => {
timeFormats.forEach(tf => {
formats.push(() =>
I18n.t('#time.event', '%{date} at %{time}', {
date: I18n.lookup(`date.formats.${df}`),
time: I18n.lookup(`time.formats.${tf}`)
})
)
})
})
return formats
}
function dateFormat(key) {
return () => I18n.lookup(`date.formats.${key}`)
}
function timeFormat(key) {
return () => I18n.lookup(`time.formats.${key}`)
}
function joinFormats(separator, ...formats) {
return () => formats.map(key => I18n.lookup(key)).join(separator)
}
const moment_formats = {
i18nToMomentHash: {
'%A': 'dddd',
'%B': 'MMMM',
'%H': 'HH',
'%M': 'mm',
'%S': 'ss',
'%P': 'a',
'%Y': 'YYYY',
'%a': 'ddd',
'%b': 'MMM',
'%m': 'M',
'%d': 'D',
'%k': 'H',
'%l': 'h',
'%z': 'Z',
'%-H': 'H',
'%-M': 'm',
'%-S': 's',
'%-m': 'M',
'%-d': 'D',
'%-k': 'H',
'%-l': 'h'
},
basicMomentFormats: [
moment.ISO_8601,
'YYYY',
'LT',
'LTS',
'L',
'l',
'LL',
'll',
'LLL',
'lll',
'LLLL',
'llll',
'D MMM YYYY',
'H:mm'
],
getFormats() {
let formatsToTransform = moment_formats.formatsForLocale()
formatsToTransform = moment_formats.formatsIncludingImplicitMinutes(formatsToTransform)
return this.transformFormats(formatsToTransform)
},
formatsIncludingImplicitMinutes(formats) {
const arrayOfArrays = _.map(formats, format =>
format.match(/:%-?M/) ? [format, format.replace(/:%-?M/, '')] : [format]
)
return _.flatten(arrayOfArrays)
},
transformFormats: _.memoize(formats => {
const localeSpecificFormats = _.map(formats, moment_formats.i18nToMomentFormat)
return _.union(moment_formats.basicMomentFormats, localeSpecificFormats)
}),
// examples are from en_US. order is significant since if an input matches
// multiple formats, the format earlier in the list will be preferred
orderedFormats: [
timeFormat('default'), // %a, %d %b %Y %H:%M:%S %z
dateFormat('full_with_weekday'), // %a %b %-d, %Y %-l:%M%P
dateFormat('full'), // %b %-d, %Y %-l:%M%P
dateFormat('date_at_time'), // %b %-d at %l:%M%P
dateFormat('long_with_weekday'), // %A, %B %-d
dateFormat('medium_with_weekday'), // %a %b %-d, %Y
dateFormat('short_with_weekday'), // %a, %b %-d
timeFormat('long'), // %B %d, %Y %H:%M
dateFormat('long'), // %B %-d, %Y
...eventTimes(['medium', 'short'], ['tiny', 'tiny_on_the_hour']),
joinFormats(' ', 'date.formats.medium', 'time.formats.tiny'),
joinFormats(' ', 'date.formats.medium', 'time.formats.tiny_on_the_hour'),
dateFormat('medium'), // %b %-d, %Y
timeFormat('short'), // %d %b %H:%M
joinFormats(' ', 'date.formats.short', 'time.formats.tiny'),
joinFormats(' ', 'date.formats.short', 'time.formats.tiny_on_the_hour'),
dateFormat('short'), // %b %-d
dateFormat('default'), // %Y-%m-%d
timeFormat('tiny'), // %l:%M%P
timeFormat('tiny_on_the_hour'), // %l%P
dateFormat('weekday'), // %A
dateFormat('short_weekday') // %a
],
formatsForLocale() {
return _.compact(moment_formats.orderedFormats.map(fn => fn()))
},
i18nToMomentFormat(fullString) {
const withEscapes = moment_formats.escapeSubStrings(fullString)
return moment_formats.replaceDateKeys(withEscapes)
},
escapeSubStrings(formatString) {
const substrings = formatString.split(' ')
const escapedSubs = _.map(substrings, moment_formats.escapedUnlessi18nKey)
return escapedSubs.join(' ')
},
escapedUnlessi18nKey(string) {
const isKey = _.find(_.keys(moment_formats.i18nToMomentHash), k => string.indexOf(k) > -1)
return isKey ? string : `[${string}]`
},
replaceDateKeys(formatString) {
return _.reduce(
moment_formats.i18nToMomentHash,
(string, forMoment, forBase) => string.replace(forBase, forMoment),
formatString
)
}
}
export default moment_formats

View File

@ -0,0 +1,45 @@
/*
* Copyright (C) 2021 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { configure } from './'
import timezone from 'timezone'
const snapshots = []
export const configureAndRestoreLater = config => snapshots.push(configure(config))
export const restore = () => snapshots.splice(0).reverse().forEach(configure)
export const changeZone = (zoneData, zoneName) => configureAndRestoreLater({
tz: timezone(zoneData, zoneName),
tzData: {
[zoneName]: zoneData
}
})
export const changeLocale = (localeData, bigeasyLocale, momentLocale) => (
configureAndRestoreLater({
tz: timezone(localeData, bigeasyLocale),
momentLocale
})
)
export default {
configureAndRestoreLater,
changeLocale,
changeZone,
restore
}

812
yarn.lock

File diff suppressed because it is too large Load Diff