package ccxt import ( "fmt" "math" "regexp" "strconv" "strings" ) const ( // TRUNCATE = 0 ROUND = 1 ROUND_UP = 2 ROUND_DOWN = 3 // DECIMAL_PLACES = 2 // SIGNIFICANT_DIGITS = 3 // TICK_SIZE = 4 // NO_PADDING = 5 // PAD_WITH_ZERO = 6 ) var precisionConstants = map[string]int{ "ROUND": ROUND, "TRUNCATE": TRUNCATE, "ROUND_UP": ROUND_UP, "ROUND_DOWN": ROUND_DOWN, "DECIMAL_PLACES": DECIMAL_PLACES, "SIGNIFICANT_DIGITS": SIGNIFICANT_DIGITS, "TICK_SIZE": TICK_SIZE, "NO_PADDING": NO_PADDING, "PAD_WITH_ZERO": PAD_WITH_ZERO, } func (this *Exchange) NumberToString(x interface{}) interface{} { res := NumberToString(x) if res == "" { return nil } return res } func NumberToString(x interface{}) string { switch v := x.(type) { case nil: return "" case float64, float32, int, int64, int32: str := fmt.Sprintf("%v", v) val := ToFloat64(v) // Handle very large numbers (positive exponents) if math.Abs(val) >= 1.0 { parts := strings.Split(str, "e") if len(parts) == 2 { // Convert the exponent into an integer exponent, _ := strconv.Atoi(parts[1]) // Split the mantissa into integer and fractional parts mantissaParts := strings.Split(parts[0], ".") integerPart := mantissaParts[0] fractionalPart := "" if len(mantissaParts) > 1 { fractionalPart = mantissaParts[1] } // Adjust the number of zeros based on the exponent if exponent >= 0 { totalDigits := integerPart + fractionalPart if exponent >= len(fractionalPart) { zerosToAdd := exponent - len(fractionalPart) return totalDigits + strings.Repeat("0", zerosToAdd) } else { return totalDigits[:len(integerPart)+exponent] + "." + totalDigits[len(integerPart)+exponent:] } } } } // Handle numbers with negative exponents (fractions) if math.Abs(val) < 1.0 { parts := strings.Split(str, "e-") if len(parts) == 2 { n := strings.Replace(parts[0], ".", "", -1) e, _ := strconv.Atoi(parts[1]) neg := str[0] == '-' if e != 0 { // Format the result with leading zeros return fmt.Sprintf("%s0.%s%s", map[bool]string{true: "-", false: ""}[neg], strings.Repeat("0", e-1), strings.Replace(n, "-", "", 1)) } } } // If no scientific notation, return the original string return str default: return fmt.Sprintf("%v", x) } } func (this *Exchange) NumberToString2(x interface{}) string { switch v := x.(type) { case nil: return "" case float64, float32, int, int64, int32: str := fmt.Sprintf("%v", v) if math.Abs(ToFloat64((v))) < 1.0 { parts := strings.Split(str, "e-") if len(parts) == 2 { n := strings.Replace(parts[0], ".", "", -1) e, _ := strconv.Atoi(parts[1]) neg := str[0] == '-' if e != 0 { // Fix: Remove the extra "-" sign in the result return fmt.Sprintf("%s0.%s%s", map[bool]string{true: "-", false: ""}[neg], strings.Repeat("0", e-1), strings.Replace(n, "-", "", 1)) } } } else { parts := strings.Split(str, "e") if len(parts) == 2 { e, _ := strconv.Atoi(parts[1]) m := strings.Split(parts[0], ".") if len(m) > 1 { e -= len(m[1]) } return fmt.Sprintf("%s%s%s", m[0], m[1], strings.Repeat("0", e)) } } return str default: return fmt.Sprintf("%v", x) } } // func (this *Exchange) NumberToString(x interface{}) string { // switch v := x.(type) { // case nil: // return "" // case float64, float32, int, int64, int32: // str := fmt.Sprintf("%v", v) // if math.Abs(ToFloat64((v))) < 1.0 { // parts := strings.Split(str, "e-") // if len(parts) == 2 { // n := strings.Replace(parts[0], ".", "", -1) // e, _ := strconv.Atoi(parts[1]) // neg := str[0] == '-' // if e != 0 { // return fmt.Sprintf("%s0.%s%s", map[bool]string{true: "-", false: ""}[neg], strings.Repeat("0", e-1), n) // } // } // } else { // parts := strings.Split(str, "e") // if len(parts) == 2 { // e, _ := strconv.Atoi(parts[1]) // m := strings.Split(parts[0], ".") // if len(m) > 1 { // e -= len(m[1]) // } // return fmt.Sprintf("%s%s%s", m[0], m[1], strings.Repeat("0", e)) // } // } // return str // default: // return fmt.Sprintf("%v", x) // } // } var truncateRegExpCache = make(map[int]*regexp.Regexp) func (this *Exchange) truncateToString(num interface{}, precision int) string { numStr := NumberToString(num) if precision > 0 { re, exists := truncateRegExpCache[precision] if !exists { re = regexp.MustCompile(fmt.Sprintf(`([-]*\d+\.\d{%d})(\d)`, precision)) truncateRegExpCache[precision] = re } match := re.FindStringSubmatch(numStr) if len(match) > 1 { return match[1] } } intNum, _ := strconv.Atoi(numStr) return strconv.Itoa(intNum) } func (this *Exchange) truncate(num interface{}, precision int) float64 { result, _ := strconv.ParseFloat(this.truncateToString(num, precision), 64) return result } func (this *Exchange) PrecisionFromString(str2 interface{}) int { str := str2.(string) if strings.ContainsAny(str, "eE") { numStr := regexp.MustCompile(`\d\.?\d*[eE]`).ReplaceAllString(str, "") precision, _ := strconv.Atoi(numStr) return -precision } split := regexp.MustCompile(`0+$`).ReplaceAllString(str, "") parts := strings.Split(split, ".") if len(parts) > 1 { return len(parts[1]) } return 0 } func getDecimalPlaces(number float64) int { str := fmt.Sprintf("%f", number) parts := strings.Split(str, ".") if len(parts) == 2 { // Count the number of decimal places by looking at the fractional part return len(strings.TrimRight(parts[1], "0")) } return 0 } func roundToDecimalPlaces(num float64, decimalPlaces int) float64 { shift := math.Pow(10, float64(decimalPlaces)) return math.Round(num*shift) / shift } func (this *Exchange) DecimalToPrecision(value interface{}, roundingMode interface{}, numPrecisionDigits interface{}, args ...interface{}) interface{} { countingMode := GetArg(args, 0, nil) paddingMode := GetArg(args, 1, nil) return this._decimalToPrecision(value, roundingMode, numPrecisionDigits, countingMode, paddingMode) } func (this *Exchange) _decimalToPrecision(x interface{}, roundingMode2, numPrecisionDigits2 interface{}, countmode2, paddingMode interface{}) string { if countmode2 == nil { countmode2 = DECIMAL_PLACES } if paddingMode == nil { paddingMode = NO_PADDING } countMode := int(ParseInt(countmode2)) roundingMode := int(ParseInt(roundingMode2)) numPrecisionDigits := ToFloat64(numPrecisionDigits2) if countMode == TICK_SIZE && numPrecisionDigits < 0 { // return "", errors.New("TICK_SIZE can't be used with negative or zero numPrecisionDigits") panic("TICK_SIZE can't be used with negative or zero numPrecisionDigits") } parsedX := ToFloat64(x) if numPrecisionDigits < 0 { toNearest := math.Pow(10, math.Abs(numPrecisionDigits)) if roundingMode == ROUND { res := this._decimalToPrecision(parsedX/toNearest, roundingMode, 0, countmode2, paddingMode) floatRes, _ := strconv.ParseFloat(res, 64) resultFloat := toNearest * floatRes resultStr := "" if resultFloat == math.Trunc(resultFloat) { resultStr = fmt.Sprintf("%d", int(resultFloat)) // Output: 10 } else { // Float value, print with decimals resultStr = fmt.Sprintf("%f", resultFloat) } return resultStr } if roundingMode == TRUNCATE { decimalPlaces := getDecimalPlaces(parsedX) modResult := roundToDecimalPlaces(math.Mod(parsedX, toNearest), decimalPlaces) // tricky go does not have fixed point types out of the box truncVal := parsedX - modResult truncValStr := "" if truncVal == math.Trunc(truncVal) { truncValStr = fmt.Sprintf("%d", int(truncVal)) // Output: 10 } else { // Float value, print with decimals truncValStr = fmt.Sprintf("%f", truncVal) } return truncValStr } } // Handle tick size if countMode == TICK_SIZE { precisionDigitsString := this._decimalToPrecision(numPrecisionDigits, ROUND, 22, DECIMAL_PLACES, NO_PADDING) newNumPrecisionDigits := this.PrecisionFromString(precisionDigitsString) missing := math.Mod(parsedX, numPrecisionDigits) missingRes := this._decimalToPrecision(missing, ROUND, 8, DECIMAL_PLACES, NO_PADDING) missingFloat, _ := strconv.ParseFloat(missingRes, 64) missing = missingFloat fpError := missing / numPrecisionDigits fpErrorStr := this._decimalToPrecision(fpError, ROUND, math.Max(float64(newNumPrecisionDigits), 8), DECIMAL_PLACES, NO_PADDING) fpErrorResult := this.PrecisionFromString(fpErrorStr) if fpErrorResult != 0 { if roundingMode == ROUND { if parsedX > 0 { if missing >= numPrecisionDigits/2 { parsedX = parsedX - missing + numPrecisionDigits } else { parsedX = parsedX - missing } } else { if missing >= numPrecisionDigits/2 { parsedX = parsedX - missing } else { parsedX = parsedX - missing - numPrecisionDigits } } } else if roundingMode == TRUNCATE { parsedX = parsedX - missing } } return this._decimalToPrecision(parsedX, ROUND, newNumPrecisionDigits, DECIMAL_PLACES, paddingMode) } // Convert to a string (if needed), skip leading minus sign (if any) str := NumberToString(x) isNegative := str[0] == '-' strStart := 0 if isNegative { strStart = 1 } strEnd := len(str) // Find the dot position in the source buffer strDot := strings.Index(str, ".") hasDot := strDot != -1 // Char code constants MINUS := byte('-') DOT := byte('.') ZERO := byte('0') ONE := byte('1') FIVE := byte('5') NINE := byte('9') // For -123.4567 the `chars` array will hold 01234567 (leading zero is reserved for rounding cases when 099 → 100) arraySize := strEnd - strStart if !hasDot { arraySize++ } chars := make([]byte, arraySize) chars[0] = ZERO // Validate & copy digits, determine certain locations in the resulting buffer afterDot := arraySize digitsStart := -1 digitsEnd := -1 for i, j := 1, strStart; j < strEnd; i, j = i+1, j+1 { value := str[j] if value == DOT { afterDot = i i-- } else if value < ZERO || value > NINE { panic("invalid number(contains an illegal character") } else { chars[i] = value if value != ZERO && digitsStart < 0 { digitsStart = i } } } if digitsStart < 0 { digitsStart = 1 } precisionStart := digitsStart if countMode == DECIMAL_PLACES { // precisionStart = afterDot + 1 precisionStart = afterDot } precisionEnd := precisionStart + int(numPrecisionDigits) // Reset the last significant digit index, as it will change during the rounding/truncation. digitsEnd = -1 allZeros := true signNeeded := isNegative for i, memo := len(chars)-1, 0; i >= 0; i-- { c := chars[i] if i != 0 { c += byte(memo) if i >= precisionStart+int(numPrecisionDigits) { ceil := roundingMode == ROUND && c >= FIVE && !(c == FIVE && memo == 1) if ceil { c = NINE + 1 } else { c = ZERO } } if c > NINE { c = ZERO memo = 1 } else { memo = 0 } } else if memo == 1 { c = ONE } chars[i] = c if c != ZERO { allZeros = false digitsStart = i if digitsEnd < 0 { digitsEnd = i + 1 } } } if countMode == SIGNIFICANT_DIGITS { precisionStart = digitsStart precisionEnd = precisionStart + int(numPrecisionDigits) } if allZeros { signNeeded = false } readStart := digitsStart if (digitsStart >= afterDot) || allZeros { readStart = afterDot - 1 } readEnd := digitsEnd if digitsEnd < afterDot { readEnd = afterDot } nSign := 0 if signNeeded { nSign = 1 } nBeforeDot := nSign + afterDot - readStart nAfterDot := int(math.Max(float64(readEnd-afterDot), 0)) actualLength := readEnd - readStart desiredLength := actualLength if paddingMode.(int) != NO_PADDING { desiredLength = precisionEnd - readStart } pad := int(math.Max(float64(desiredLength-actualLength), 0)) padStart := nBeforeDot + 1 + nAfterDot padEnd := padStart + pad isInteger := nAfterDot+pad == 0 offsetInt := 0 if isInteger { offsetInt = 0 } else { offsetInt = 1 } outArray := make([]byte, nBeforeDot+(offsetInt)+nAfterDot+pad) // ------------------------------------------------------------------------------------------ // --------------------- if signNeeded { outArray[0] = MINUS // - minus sign } for i, j := nSign, readStart; i < nBeforeDot; i, j = i+1, j+1 { outArray[i] = chars[j] // 123 before dot } if !isInteger { outArray[nBeforeDot] = DOT // . dot } for i, j := nBeforeDot+1, afterDot; i < padStart; i, j = i+1, j+1 { outArray[i] = chars[j] // 456 after dot } for i := padStart; i < padEnd; i++ { outArray[i] = ZERO // 000 padding } return string(outArray) } // func (this *Exchange) _decimalToPrecision(x interface{}, roundingMode interface{}, numPrecisionDigits2 interface{}, countingMode2 interface{}, paddingMode2 interface{}) string { // countingMode := countingMode2.(int) // paddingMode := paddingMode2.(int) // numPrecisionDigits := numPrecisionDigits2 // floatNumPrecisionDigits := numPrecisionDigits.(float64) // if countingMode == TICK_SIZE { // // if numPrecisionDigitsStr, ok := strconv.Itoa(numPrecisionDigits); ok { // // numPrecisionDigits, _ = strconv.ParseFloat(numPrecisionDigitsStr, 64) // // } // if numPrecisionDigits.(float64) <= 0 { // return "" // } // } // if floatNumPrecisionDigits < 0 { // toNearest := math.Pow(10, float64(-floatNumPrecisionDigits)) // if roundingMode == ROUND { // return this.DecimalToPrecision(x.(float64)/toNearest*toNearest, roundingMode, 0, countingMode, paddingMode) // } // if roundingMode == TRUNCATE { // return fmt.Sprintf("%v", x.(float64)-math.Mod(x.(float64), toNearest)) // } // } // str := this.NumberToString(x) // isNegative := str[0] == '-' // strStart := 0 // if isNegative { // strStart = 1 // } // strEnd := len(str) // var strDot int // // hasDot := false // for strDot = 0; strDot < strEnd; strDot++ { // if str[strDot] == '.' { // // hasDot = true // break // } // } // chars := make([]uint8, strEnd-strStart) // chars[0] = '0' // afterDot := len(chars) // digitsStart, digitsEnd := -1, -1 // for i, j := 1, strStart; j < strEnd; j, i = j+1, i+1 { // c := str[j] // if c == '.' { // afterDot = i // i-- // } else { // chars[i] = c // if c != '0' && digitsStart < 0 { // digitsStart = i // } // } // } // if digitsStart < 0 { // digitsStart = 1 // } // precisionStart := afterDot // if countingMode == SIGNIFICANT_DIGITS { // precisionStart = digitsStart // } // precisionEnd := precisionStart + numPrecisionDigits // digitsEnd = -1 // allZeros := true // signNeeded := isNegative // for i, memo := len(chars)-1, 0; i >= 0; i-- { // c := chars[i] // if i != 0 { // c += uint8(memo) // if i >= (precisionStart + numPrecisionDigits) { // ceil := (roundingMode == ROUND) && (c >= '5') && !(c == '5' && memo != 0) // if ceil { // c = '0' // } else { // c = '0' // } // } // if c > '9' { // c = '0' // memo = 1 // } else { // memo = 0 // } // } else if memo != 0 { // c = '1' // } // chars[i] = c // if c != '0' { // allZeros = false // digitsStart = i // if digitsEnd < 0 { // digitsEnd = i + 1 // } // } // } // if countingMode == SIGNIFICANT_DIGITS { // precisionStart = digitsStart // precisionEnd = precisionStart + numPrecisionDigits // } // if allZeros { // signNeeded = false // } // readStart := afterDot - 1 // if digitsStart < afterDot || !allZeros { // readStart = digitsStart // } // readEnd := afterDot // if digitsEnd >= afterDot { // readEnd = digitsEnd // } // nSign := 0 // if signNeeded { // nSign = 1 // } // nBeforeDot := nSign + (afterDot - readStart) // nAfterDot := readEnd - afterDot // actualLength := readEnd - readStart // desiredLength := actualLength // if paddingMode != NO_PADDING { // desiredLength = precisionEnd - readStart // } // pad := desiredLength - actualLength // // padStart := nBeforeDot + 1 + nAfterDot // // padEnd := padStart + pad // isInteger := nAfterDot+pad == 0 // out := make([]uint8, nBeforeDot) // if !isInteger { // out = append(out, '.') // } // out = append(out, chars[readStart:readEnd]...) // for i := 0; i < pad; i++ { // out = append(out, '0') // } // if signNeeded { // return fmt.Sprintf("-%s", string(out)) // } // return string(out) // } // func (this *Exchange) omitZero(stringNumber string) string { // if stringNumber == "" { // return "" // } // num, err := strconv.ParseFloat(stringNumber, 64) // if err != nil || num == 0 { // return "" // } // return stringNumber // }