Files
aiaa-notification-server/internal/display/price.go
T
ryan f8e74738cc feat(价格展示): 避免低价资产被格式化丢失为 0.00
Motivation:
交易通知等展示场景中,低价资产(如 0.00000059)按保留两位小数的格式渲染会被截断为 0.00,用户无法读取真实价格;需要在不改变存量数值类型和既有模板写法的前提下,保证任意精度价格都能正确展示。

Changes:

* 新增价格展示格式化能力:数值不小于 1 时保留两位小数,小于 1 时输出最短精确值
* 新增 Decimal 类型实现自定义格式化,使模板中的浮点值不再坍缩为 0.00 且不产生科学计数法
* 新增模板数据渲染期数值包装能力,不修改原始数据,保证条件判断与既有模板语法兼容
* 补充价格格式化、精度保留及数据不可变性的单元测试
2026-08-23 00:17:43 +08:00

114 lines
2.3 KiB
Go

package display
import (
"fmt"
"io"
"math"
"strconv"
"strings"
)
// FormatPrice renders a price for display. Values >= 1 keep two decimals for
// readability; smaller values use the shortest exact representation so tiny
// prices like 0.00000059 are not collapsed to 0.00.
func FormatPrice(v float64) string {
if v == 0 {
return "0.00"
}
if math.Abs(v) >= 1 {
return strconv.FormatFloat(v, 'f', 2, 64)
}
return strconv.FormatFloat(v, 'f', -1, 64)
}
// Decimal is a float64 that prints without collapsing sub-0.01 values to 0.00
// and without scientific notation. Templates keep using {{printf "%.2f" .price}}
// and {{.price}}; wrapping happens at render time so stored data stays numeric.
type Decimal float64
func (d Decimal) Format(f fmt.State, verb rune) {
v := float64(d)
switch verb {
case 'f', 'F':
prec := 6
if p, ok := f.Precision(); ok {
prec = p
}
s := strconv.FormatFloat(v, byte(verb), prec, 64)
if v != 0 && isCollapsedZero(s) {
s = strconv.FormatFloat(v, 'f', -1, 64)
}
_, _ = io.WriteString(f, s)
case 'v', 's':
_, _ = io.WriteString(f, formatPlain(v))
default:
prec := -1
if p, ok := f.Precision(); ok {
prec = p
}
_, _ = io.WriteString(f, strconv.FormatFloat(v, byte(verb), prec, 64))
}
}
func formatPlain(v float64) string {
if v == 0 {
return "0"
}
return strconv.FormatFloat(v, 'f', -1, 64)
}
func isCollapsedZero(s string) bool {
t := strings.TrimPrefix(s, "-")
t = strings.TrimPrefix(t, "+")
if t == "" {
return false
}
sawZero := false
for _, r := range t {
switch r {
case '0':
sawZero = true
case '.':
default:
return false
}
}
return sawZero
}
// WrapMap copies data and wraps float values so template printing keeps
// precision. The original map is left unchanged for condition evaluation.
func WrapMap(data map[string]interface{}) map[string]interface{} {
if data == nil {
return nil
}
out := make(map[string]interface{}, len(data))
for k, v := range data {
out[k] = wrapValue(v)
}
return out
}
func wrapValue(v any) any {
switch t := v.(type) {
case nil:
return nil
case float64:
return Decimal(t)
case float32:
return Decimal(t)
case Decimal:
return t
case map[string]interface{}:
return WrapMap(t)
case []interface{}:
out := make([]interface{}, len(t))
for i, x := range t {
out[i] = wrapValue(x)
}
return out
default:
return v
}
}