-
Notifications
You must be signed in to change notification settings - Fork 4
strict mode for parsing numbers #6
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
base: master
Are you sure you want to change the base?
Changes from all commits
87d15c6
604707f
9464034
707532f
8cffa15
1a379c6
8f6cdab
3321734
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,37 +8,46 @@ const cssResolutionUnits: string[] = require('css-resolution-units'); | |
const cssFrequencyUnits: string[] = require('css-frequency-units'); | ||
const cssTimeUnits: string[] = require('css-time-units'); | ||
|
||
const numberPrefixPattern = /^(\+|-)?(\.)?\d/; | ||
|
||
export interface IOptions { | ||
strict?: boolean; | ||
} | ||
|
||
export default class CssDimension { | ||
|
||
public static parse(value: string) { | ||
return new CssDimension(value); | ||
public static parse(value: string, options?: IOptions) { | ||
return new CssDimension(value, options); | ||
} | ||
|
||
public type: string; | ||
public value: number; | ||
public unit: string; | ||
|
||
constructor(value: string) { | ||
constructor(value: string, options?: IOptions) { | ||
|
||
this.validateNumber(value); | ||
this.validateSign(value); | ||
this.validateDots(value); | ||
|
||
const strict = !!(options && options.strict); | ||
|
||
if (/%$/.test(value)) { | ||
this.type = 'percentage'; | ||
this.value = tryParseFloat(value); | ||
this.value = tryParseNumber(value.substring(0, value.length - 1), strict); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's going on here with the substring? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it removes the percent sign so that (potentially) only the number itself will be parsed (removal of the percent sign could be omitted in non-strict mode) |
||
this.unit = '%'; | ||
return; | ||
} | ||
|
||
const unit = parseUnit(value); | ||
if (!unit) { | ||
this.type = 'number'; | ||
this.value = tryParseFloat(value); | ||
this.value = tryParseNumber(value, strict); | ||
return; | ||
} | ||
|
||
this.type = unitToType(unit); | ||
this.value = tryParseFloat(value.substr(0, value.length - unit.length)); | ||
this.value = tryParseNumber(value.substr(0, value.length - unit.length), strict); | ||
this.unit = unit; | ||
} | ||
|
||
|
@@ -74,6 +83,10 @@ function countDots(value: string) { | |
return m ? m.length : 0; | ||
} | ||
|
||
function tryParseNumber(value: string, strict: boolean) { | ||
return strict ? tryParseStrict(value) : tryParseFloat(value); | ||
} | ||
|
||
function tryParseFloat(value: string) { | ||
const result = parseFloat(value); | ||
if (isNaN(result)) { | ||
|
@@ -82,6 +95,59 @@ function tryParseFloat(value: string) { | |
return result; | ||
} | ||
|
||
function normalizeNumber(value: string, allowDot: boolean = true) { | ||
value = value[0] === '0' ? value.replace(/^0+(\d)/, '$1') : value; | ||
const match = numberPrefixPattern.exec(value); | ||
if (!match) { | ||
return null; | ||
} | ||
const [, sign, dot] = match; | ||
if (sign === '+') { | ||
value = value.substr(1); | ||
} | ||
if (dot) { | ||
if (!allowDot) { | ||
return null; | ||
} | ||
if (sign === '-') { | ||
value = '-0' + value.substr(1); | ||
} else { | ||
value = '0' + value; | ||
} | ||
} | ||
return (dot || countDots(value)) | ||
? value.replace(/\.?0+$/, '') | ||
: value; | ||
} | ||
|
||
function tryParseStrict(value: string) { | ||
const nval = normalizeNumber(value); | ||
if (!nval) { | ||
throw new Error(`Invalid number: ${value}`); | ||
} | ||
const result = parseFloat(nval); | ||
if (result.toString() !== nval && !verifyZero(value) && !verifyENotation(value)) { | ||
throw new Error(`Invalid number: ${value}`); | ||
} | ||
return result; | ||
} | ||
|
||
function verifyZero(value: string) { | ||
return /^[-+]?0\.0+$/.test(value); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The following works w/o breaking tests: return parseFloat(value) === 0; There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess so, but There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, this is the discussion I was trying to spark here. Those other cases might be worth testing. You could also consider |
||
} | ||
|
||
function verifyENotation(value: string) { | ||
const m = /e/i.exec(value); | ||
if (!m || m.index === value.length - 1) { | ||
return false; | ||
} | ||
const nval = normalizeNumber(value.substring(m.index + 1), false); | ||
if (!nval) { | ||
throw new Error(`Invalid number: ${value}`); | ||
} | ||
return parseInt(nval, 10).toString() === nval; | ||
} | ||
|
||
const units = cssAngleUnits.concat( | ||
cssFrequencyUnits, | ||
cssLengthUnits, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this so far away from the implementation? Also, I can remove the trailing
\d
and all the tests still pass.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, there needs to be be test added for this:
without this you could enter "NaN%" and it would falsely validate it a number