-
Notifications
You must be signed in to change notification settings - Fork 74
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #858 from illacloud/beta
fix: 🐛 input number
- Loading branch information
Showing
6 changed files
with
530 additions
and
141 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,261 @@ | ||
import { | ||
toSafeString, | ||
trimNumber, | ||
validateNumber, | ||
getNumberPrecision, | ||
supportBigInt, | ||
} from "./utils" | ||
|
||
export class BigIntDecimal { | ||
readonly isEmpty: boolean = false | ||
|
||
readonly isNaN: boolean = false | ||
|
||
private readonly isNegative: boolean = false | ||
|
||
private readonly origin: string = "" | ||
|
||
private readonly integer!: bigint | ||
|
||
private readonly decimal!: bigint | ||
|
||
private readonly decimalLen!: number | ||
|
||
constructor(value: string | number) { | ||
this.origin = String(value) | ||
|
||
if ((!value && value !== 0) || !this.origin.trim()) { | ||
this.isEmpty = true | ||
return | ||
} | ||
|
||
if (value === "-") { | ||
this.isNaN = true | ||
return | ||
} | ||
|
||
const safeValueString = toSafeString(value) | ||
if (validateNumber(safeValueString)) { | ||
const { negative, trimStr } = trimNumber(safeValueString) | ||
const [integerStr, decimalStr = "0"] = trimStr.split(".") | ||
this.isNegative = negative | ||
this.integer = BigInt(integerStr) | ||
this.decimal = BigInt(decimalStr) | ||
this.decimalLen = decimalStr.length | ||
} else { | ||
this.isNaN = true | ||
} | ||
} | ||
|
||
get isInvalid() { | ||
return this.isEmpty || this.isNaN | ||
} | ||
|
||
private getMark() { | ||
return this.isNegative ? "-" : "" | ||
} | ||
|
||
private getIntegerStr() { | ||
return this.integer.toString() | ||
} | ||
|
||
private getDecimalStr() { | ||
return this.decimal.toString().padStart(this.decimalLen, "0") | ||
} | ||
|
||
private alignDecimal(decimalLength: number): bigint { | ||
return BigInt( | ||
`${this.getMark()}${this.getIntegerStr()}${this.getDecimalStr().padEnd( | ||
decimalLength, | ||
"0", | ||
)}`, | ||
) | ||
} | ||
|
||
negate() { | ||
const numStr = this.toString() | ||
return new BigIntDecimal( | ||
numStr.startsWith("-") ? numStr.slice(1) : `-${numStr}`, | ||
) | ||
} | ||
|
||
add(value: string | number): BigIntDecimal { | ||
const offset = new BigIntDecimal(value) | ||
|
||
if (offset.isInvalid) { | ||
return this | ||
} | ||
|
||
if (this.isInvalid) { | ||
return offset | ||
} | ||
|
||
const maxDecimalLength = Math.max(this.decimalLen, offset.decimalLen) | ||
const thisAlignedDecimal = this.alignDecimal(maxDecimalLength) | ||
const offsetAlignedDecimal = offset.alignDecimal(maxDecimalLength) | ||
const valueStr = (thisAlignedDecimal + offsetAlignedDecimal).toString() | ||
const { negativeStr, trimStr } = trimNumber(valueStr) | ||
const hydrateValueStr = `${negativeStr}${trimStr.padStart( | ||
maxDecimalLength + 1, | ||
"0", | ||
)}` | ||
|
||
return new BigIntDecimal( | ||
`${hydrateValueStr.slice(0, -maxDecimalLength)}.${hydrateValueStr.slice( | ||
-maxDecimalLength, | ||
)}`, | ||
) | ||
} | ||
|
||
equals(target: BigIntDecimal) { | ||
return this.toString() === target?.toString() | ||
} | ||
|
||
less(target: BigIntDecimal) { | ||
return this.isInvalid || target.isInvalid | ||
? false | ||
: this.add(target.negate().toString()).toNumber() < 0 | ||
} | ||
|
||
toNumber(): number { | ||
return this.isNaN ? NaN : Number(this.toString()) | ||
} | ||
|
||
toString( | ||
options: { safe: boolean; precision?: number } = { safe: true }, | ||
): string { | ||
const { safe, precision } = options | ||
const result = safe | ||
? this.isInvalid | ||
? "" | ||
: trimNumber( | ||
`${this.getMark()}${this.getIntegerStr()}.${this.getDecimalStr()}`, | ||
).fullStr | ||
: this.origin | ||
return typeof precision === "number" ? toFixed(result, precision) : result | ||
} | ||
} | ||
|
||
export class NumberDecimal { | ||
readonly isEmpty: boolean = false | ||
|
||
readonly isNaN: boolean = false | ||
|
||
private readonly origin: string = "" | ||
|
||
private readonly number: number | ||
|
||
constructor(value: string | number) { | ||
this.origin = String(value) | ||
this.number = Number(value) | ||
|
||
if ((!value && value !== 0) || !this.origin.trim()) { | ||
this.isEmpty = true | ||
} else { | ||
this.isNaN = Number.isNaN(this.number) | ||
} | ||
} | ||
|
||
get isInvalid() { | ||
return this.isEmpty || this.isNaN | ||
} | ||
|
||
negate() { | ||
return new NumberDecimal(-this.toNumber()) | ||
} | ||
|
||
equals(target: NumberDecimal) { | ||
return this.toNumber() === target?.toNumber() | ||
} | ||
|
||
less(target: NumberDecimal) { | ||
return this.isInvalid || target.isInvalid | ||
? false | ||
: this.add(target.negate().toString()).toNumber() < 0 | ||
} | ||
|
||
add(value: string | number): NumberDecimal { | ||
const offset = new NumberDecimal(value) | ||
|
||
if (offset.isInvalid) { | ||
return this | ||
} | ||
|
||
if (this.isInvalid) { | ||
return offset | ||
} | ||
|
||
const result = this.number + offset.number | ||
if (result > Number.MAX_SAFE_INTEGER) { | ||
return new NumberDecimal(Number.MAX_SAFE_INTEGER) | ||
} | ||
|
||
if (result < Number.MIN_SAFE_INTEGER) { | ||
return new NumberDecimal(Number.MIN_SAFE_INTEGER) | ||
} | ||
|
||
const maxPrecision = Math.max( | ||
getNumberPrecision(this.number), | ||
getNumberPrecision(offset.number), | ||
) | ||
return new NumberDecimal(result.toFixed(maxPrecision)) | ||
} | ||
|
||
toNumber() { | ||
return this.number | ||
} | ||
|
||
toString(options: { safe: boolean; precision?: number } = { safe: true }) { | ||
const { safe, precision } = options | ||
const result = safe | ||
? this.isInvalid | ||
? "" | ||
: toSafeString(this.number) | ||
: this.origin | ||
return typeof precision === "number" ? toFixed(result, precision) : result | ||
} | ||
} | ||
|
||
export function getDecimal(value: string | number) { | ||
return supportBigInt() ? new BigIntDecimal(value) : new NumberDecimal(value) | ||
} | ||
|
||
/** | ||
* Replace String.prototype.toFixed like Math.round | ||
* If cutOnly is true, just slice the tail | ||
* e.g. Decimal.toFixed(0.15) will return 0.2, not 0.1 | ||
*/ | ||
export function toFixed( | ||
numStr: string, | ||
precision?: number, | ||
cutOnly = false, | ||
): string { | ||
if (numStr === "") { | ||
return "" | ||
} | ||
|
||
const separator = "." | ||
const { negativeStr, integerStr, decimalStr } = trimNumber(numStr) | ||
const precisionDecimalStr = `${separator}${decimalStr}` | ||
const numberWithoutDecimal = `${negativeStr}${integerStr}` | ||
|
||
if (precision && precision >= 0) { | ||
const advancedNum = Number(decimalStr[precision]) | ||
if (advancedNum >= 5 && !cutOnly) { | ||
const advancedDecimal = getDecimal(numStr).add( | ||
`${negativeStr}0.${"0".repeat(precision)}${10 - advancedNum}`, | ||
) | ||
return toFixed(advancedDecimal.toString(), precision, cutOnly) | ||
} | ||
|
||
return precision === 0 | ||
? numberWithoutDecimal | ||
: `${numberWithoutDecimal}${separator}${decimalStr | ||
.padEnd(precision, "0") | ||
.slice(0, precision)}` | ||
} | ||
|
||
return `${numberWithoutDecimal}${ | ||
precisionDecimalStr === ".0" ? "" : precisionDecimalStr | ||
}` | ||
} |
Oops, something went wrong.