Skip to content

Commit

Permalink
Merge branch 'fix/number_input' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
AruSeito committed Feb 18, 2024
2 parents 75dee71 + 3e1775f commit f0091d5
Show file tree
Hide file tree
Showing 6 changed files with 530 additions and 141 deletions.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
![cover](./.github/assets/images/design-cover.png)

[![Chat on Discord](https://img.shields.io/badge/chat-Discord-7289DA?logo=discord)](https://discord.gg/illacloud)
[![Follow on Twitter](https://img.shields.io/badge/Twitter-1DA1F2?logo=twitter&logoColor=white)](https://twitter.com/illacloudHQ)
[![storybook](./.github/assets/images/storybook.svg)](https://design.illafamily.com)
[![codecov](https://codecov.io/gh/illacloud/illa-design/branch/main/graph/badge.svg?token=GR2SOLBWQN)](https://codecov.io/gh/illacloud/illa-design)
[![license](https://img.shields.io/github/license/illacloud/illa-design)](./LICENSE)
Expand Down
4 changes: 3 additions & 1 deletion apps/storybook/stories/input-number/input-number.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ export const Basic: StoryFn<InputNumberProps> = (props) => {
/>
<InputNumber
w="320px"
value={currentValue}
{...props}
precision={3}
value={currentValue}
onChange={(v) => {
console.log("v", v)
setCurrentValue(v)
}}
mode="button"
Expand Down
261 changes: 261 additions & 0 deletions packages/input-number/src/Decimal.ts
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
}`
}
Loading

0 comments on commit f0091d5

Please sign in to comment.