Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix issue #1596 - Provide way to specify multiple formats when creating a date in UTC #1914

Open
wants to merge 25 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
54164cc
chore(release): 1.11.5 [skip ci]
semantic-release-bot Aug 12, 2022
52e42e7
Merge branch 'master' of github.com:iamkun/dayjs
BePo65 Oct 10, 2022
29813a8
Merge branch 'master' of github.com:iamkun/dayjs
BePo65 Oct 21, 2022
33fbeae
Merge branch 'master' of github.com:iamkun/dayjs
BePo65 Jan 7, 2023
5ff4326
test: update duplicate test descriptions
BePo65 May 8, 2022
27cf19f
test: move tests for utc with customParseFormat to separate file
BePo65 May 14, 2022
9925918
fix: parsing with multiple formats does not respect 'utc'
BePo65 May 17, 2022
4835039
test: reorder and rename tests
BePo65 May 17, 2022
209fb14
fix: arguments is of type object, but parse expects an array
BePo65 May 17, 2022
006af9a
fix: just using c parameter creates self reference
BePo65 May 17, 2022
f6dee59
fix: taking care of date with 'Z' (offset +00:00) in strict mode
BePo65 May 17, 2022
6ce64f2
test: add test for offset + strict and without utc
BePo65 May 17, 2022
f7b8488
fix: disallow overflow on customParseFormat with 'strict'
BePo65 May 21, 2022
a45fced
fix: handle parsing of token, when not found at beginning of input
BePo65 May 21, 2022
bf167af
test: add tests for incomatibilities with moment.js
BePo65 May 21, 2022
c6d7ce3
test: remove 'strict mode' tests (are now part of 'compatibility')
BePo65 May 21, 2022
0ada826
test: moved test 'invalid Dates' to 'incomatibilities' tests
BePo65 May 21, 2022
0aa776d
test: add new tests for 'strict mode'
BePo65 May 21, 2022
9fb3c2b
test: add test cases for customParseFormat
BePo65 May 22, 2022
1d6aa7c
test: move test case 'parse X x' to the end of the file
BePo65 May 22, 2022
8118da1
test: improve code coverage for customParseFormat
BePo65 May 24, 2022
5f5dbd9
refactor: replace spread operator for compatibility with older js ver…
BePo65 Jun 5, 2022
375d551
fix: error reported by eslint (line-length)
BePo65 Jun 6, 2022
47a683b
test: add tests for customParseFormat.utils.js
BePo65 Jun 6, 2022
af75753
test: add tests for issue #1734
BePo65 Mar 25, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,15 @@ const dayjs = function (date, c) {
return date.clone()
}
// eslint-disable-next-line no-nested-ternary
const cfg = typeof c === 'object' ? c : {}
let cfg = {}
if ((typeof c === 'object') && !Array.isArray(c)) {
// clone c (contains cfg object)
cfg = JSON.parse(JSON.stringify(c))
}
cfg.date = date
cfg.args = arguments// eslint-disable-line prefer-rest-params
if (cfg.args === undefined) {
cfg.args = Array.prototype.slice.call(arguments) // eslint-disable-line prefer-rest-params
}
return new Dayjs(cfg) // eslint-disable-line no-use-before-define
}

Expand Down
82 changes: 60 additions & 22 deletions src/plugin/customParseFormat/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { u } from '../localizedFormat/utils'
import { daysInMonth } from './utils'

const formattingTokens = /(\[[^[]*\])|([-_:/.,()\s]+)|(A|a|YYYY|YY?|MM?M?M?|Do|DD?|hh?|HH?|mm?|ss?|S{1,3}|z|ZZ?)/g

Expand Down Expand Up @@ -138,6 +139,30 @@ function correctHours(time) {
}
}

function checkOverflow(year, month, day, hours, minutes, seconds, milliseconds) {
let overflow = false
if (month) {
overflow = overflow || (month < 0 || month > 12)
}
if ((day !== undefined) && (day !== null)) {
overflow = overflow || (day < 1 || day > daysInMonth(year, month))
}
if (hours) {
overflow = overflow || (hours < 0 || hours > 24 ||
(hours === 24 && (minutes !== 0 || seconds !== 0 || milliseconds !== 0)))
}
if (minutes) {
overflow = overflow || (minutes < 0 || minutes > 59)
}
if (seconds) {
overflow = overflow || (seconds < 0 || seconds > 59)
}
if (milliseconds) {
overflow = overflow || (milliseconds < 0 || milliseconds > 999)
}
return overflow
}

function makeParser(format) {
format = u(format, locale && locale.formats)
const array = format.match(formattingTokens)
Expand All @@ -154,32 +179,48 @@ function makeParser(format) {
}
}
return function (input) {
const time = {}
for (let i = 0, start = 0; i < length; i += 1) {
const time = { strictMatch: true }
let start = 0
for (let i = 0; i < length; i += 1) {
const token = array[i]
if (typeof token === 'string') {
const separator = input.slice(start, start + token.length)
time.strictMatch = time.strictMatch && (separator === token)
start += token.length
} else {
const { regex, parser } = token
const part = input.slice(start)
const match = regex.exec(part)
const value = match[0]
parser.call(time, value)
input = input.replace(value, '')
if (match !== null) {
const value = match[0] // eslint-disable-line prefer-destructuring
parser.call(time, value)
input = input.replace(value, '')
if (match.index !== 0) {
time.strictMatch = false
start += match.index
}
} else {
time.strictMatch = false
break
}
}
}
if (start < input.length) {
time.strictMatch = false
}
correctHours(time)
return time
}
}

const parseFormattedInput = (input, format, utc) => {
const parseFormattedInput = (input, format, utc, strict) => {
try {
if (['x', 'X'].indexOf(format) > -1) return new Date((format === 'X' ? 1000 : 1) * input)
const parser = makeParser(format)
const {
year, month, day, hours, minutes, seconds, milliseconds, zone
year, month, day, hours, minutes, seconds, milliseconds, zone, strictMatch
} = parser(input)
const isOverflow = checkOverflow(year, month, day, hours, minutes, seconds, milliseconds)
const now = new Date()
const d = day || ((!year && !month) ? now.getDate() : 1)
const y = year || now.getFullYear()
Expand All @@ -191,19 +232,22 @@ const parseFormattedInput = (input, format, utc) => {
const m = minutes || 0
const s = seconds || 0
const ms = milliseconds || 0
if (zone) {
return new Date(Date.UTC(y, M, d, h, m, s, ms + (zone.offset * 60 * 1000)))
}
if (utc) {
return new Date(Date.UTC(y, M, d, h, m, s, ms))
let parsedDate = new Date('') // Invalid Date
if (!strict || (strict && strictMatch && !isOverflow)) {
if (zone) {
parsedDate = new Date(Date.UTC(y, M, d, h, m, s, ms + (zone.offset * 60 * 1000)))
} else if (utc) {
parsedDate = new Date(Date.UTC(y, M, d, h, m, s, ms))
} else {
parsedDate = new Date(y, M, d, h, m, s, ms)
}
}
return new Date(y, M, d, h, m, s, ms)
return parsedDate
} catch (e) {
return new Date('') // Invalid Date
}
}


export default (o, C, d) => {
d.p.customParseFormat = true
if (o && o.parseTwoDigitYear) {
Expand All @@ -229,22 +273,16 @@ export default (o, C, d) => {
if (!isStrictWithoutLocale && pl) {
locale = d.Ls[pl]
}
this.$d = parseFormattedInput(date, format, utc)
this.$d = parseFormattedInput(date, format, utc, isStrict)
this.init()
if (pl && pl !== true) this.$L = this.locale(pl).$L
// use != to treat
// input number 1410715640579 and format string '1410715640579' equal
// eslint-disable-next-line eqeqeq
if (isStrict && date != this.format(format)) {
this.$d = new Date('')
}
// reset global locale to make parallel unit test
locale = {}
} else if (format instanceof Array) {
const len = format.length
for (let i = 1; i <= len; i += 1) {
args[1] = format[i - 1]
const result = d.apply(this, args)
const result = d.apply(this, [date, cfg])
if (result.isValid()) {
this.$d = result.$d
this.$L = result.$L
Expand Down
26 changes: 26 additions & 0 deletions src/plugin/customParseFormat/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// handle negative values when overflowing month (handling positive and negative values)
function modMonth(n, x) {
return ((n % x) + x) % x
}

// code duplication as we cannot use 'isLeapYear' plugin here;
// 'isLeapYear' is only working with Dayjs objects
function isLeapYear(year) {
return ((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0)
}

// eslint-disable-next-line import/prefer-default-export
export const daysInMonth = (year, month) => {
Copy link
Owner

@iamkun iamkun Jun 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can use the built-in API

dayjs(year + '-' + month).daysInMonth()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry for the late reply, but somehow I missed this comment :-(

Of course we could use dayjs(year + '-' + month).daysInMonth() instead of my 'auxiliary' method.

But the problem is that we do not have the dayjs object available to the checkOverflow() function.

Therefore I created the daysInMonth method in the customParseFormat/utils file.
As an additional bonus, this function runs faster than creating a new dayjs object (to be exact, more than 1 instance is created for getting daysInMonth).

What do you think about this?

if (Number.isNaN(year) || Number.isNaN(month)) {
return NaN
}
const monthAsIndex = month - 1
const monthInYear = modMonth(monthAsIndex, 12)
year += (monthAsIndex - monthInYear) / 12
// eslint-disable-next-line no-nested-ternary
return monthInYear === 1
? isLeapYear(year)
? 29
: 28
: 31 - ((monthInYear % 7) % 2)
}
3 changes: 2 additions & 1 deletion src/plugin/utc/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ function offsetFromString(value = '') {
export default (option, Dayjs, dayjs) => {
const proto = Dayjs.prototype
dayjs.utc = function (date) {
const cfg = { date, utc: true, args: arguments } // eslint-disable-line prefer-rest-params
// eslint-disable-next-line prefer-rest-params
const cfg = { date, utc: true, args: Array.prototype.slice.call(arguments) }
return new Dayjs(cfg) // eslint-disable-line no-use-before-define
}

Expand Down
2 changes: 1 addition & 1 deletion test/comparison.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ test('is after invalid', () => {

// isBefore()

test('is after without units', () => {
test('is before without units', () => {
const m = dayjs(new Date(2011, 3, 2, 3, 4, 5, 10))
const mCopy = dayjs(m)
expect(m.isBefore(dayjs(new Date(2012, 3, 2, 3, 5, 5, 10)))).toBe(true, 'year is later')
Expand Down
Loading