5 小时以前 9bad721754fe8bbe2e5f459d0706e0fefac569f3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
package cn.iocoder.yudao.module.qcreport.engine;
 
import java.math.BigDecimal;
import java.time.temporal.TemporalAccessor;
import java.time.format.DateTimeFormatter;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
 
/**
 * 上下文取值。
 * <p>
 * 只做白名单式的属性导航,不解析、不执行任何代码:
 * 支持 a.b.c 与 a.b[0].c 两种写法,路径在数组上继续取属性时按元素逐个取值(pluck)。
 * <p>
 * 与前端 {@code src/components/quality/engine/path.ts} 一一对应,两边必须同语义。
 */
public final class Paths {
 
    /** 路径片段:普通属性名或数组下标 */
    private static final Pattern TOKEN_PATTERN = Pattern.compile("[^.\\[\\]]+");
 
    /** 禁止访问的属性名,避免顺着原型链读到构造器 */
    private static final Set<String> BLOCKED_KEYS = Set.of("__proto__", "constructor", "prototype");
 
    private static final Pattern INTEGER_TEXT = Pattern.compile("^\\d+$");
 
    private static final DateTimeFormatter DATE_TIME_FORMAT =
            DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
 
    private Paths() {
    }
 
    /**
     * 把路径拆成片段:inspectionItems[0].actualValue → [inspectionItems, 0, actualValue]
     */
    public static List<String> parsePath(String path) {
        List<String> tokens = new ArrayList<>();
        if (path == null) {
            return tokens;
        }
        Matcher matcher = TOKEN_PATTERN.matcher(path);
        while (matcher.find()) {
            tokens.add(matcher.group());
        }
        return tokens;
    }
 
    /**
     * 按路径取值,取不到返回 null。
     */
    public static Object readPath(Object source, String path) {
        return readTokens(source, parsePath(path), 0);
    }
 
    private static Object readTokens(Object source, List<String> tokens, int start) {
        Object current = source;
        for (int index = start; index < tokens.size(); index++) {
            String token = tokens.get(index);
            if (current == null) {
                return null;
            }
            if (current instanceof List<?> list) {
                Integer arrayIndex = toIndex(token);
                if (arrayIndex == null) {
                    // 数组上继续取属性:逐元素取值,得到同长度的数组
                    List<Object> plucked = new ArrayList<>(list.size());
                    for (Object element : list) {
                        plucked.add(readTokens(element, tokens, index));
                    }
                    return plucked;
                }
                current = arrayIndex < list.size() ? list.get(arrayIndex) : null;
                continue;
            }
            if (current instanceof Map<?, ?> map) {
                if (BLOCKED_KEYS.contains(token)) {
                    return null;
                }
                current = map.get(token);
                continue;
            }
            // 标量后面还有路径片段,说明路径写错了
            return null;
        }
        return current;
    }
 
    private static Integer toIndex(String token) {
        return INTEGER_TEXT.matcher(token).matches() ? Integer.valueOf(token) : null;
    }
 
    /**
     * 展示用格式化:空值渲染为空字符串,布尔值转中文,日期只保留到秒。
     * <p>
     * 整数值的双精度数(如 60.0)按整数输出,与 JS 的 {@code String(60)} 保持一致,
     * 否则报告上会出现「标准值 60.0」这种不该有的尾数。
     */
    public static String formatValue(Object value) {
        if (value == null) {
            return "";
        }
        if (value instanceof Boolean bool) {
            return bool ? "是" : "否";
        }
        if (value instanceof Date date) {
            return DATE_TIME_FORMAT.format(date.toInstant().atZone(ZoneId.systemDefault()));
        }
        if (value instanceof TemporalAccessor temporal) {
            return DATE_TIME_FORMAT.format(temporal);
        }
        if (value instanceof Collection<?> collection) {
            List<String> parts = new ArrayList<>(collection.size());
            for (Object element : collection) {
                parts.add(formatValue(element));
            }
            return String.join("、", parts);
        }
        return formatNumberAware(value);
    }
 
    private static String formatNumberAware(Object value) {
        if (value instanceof Double || value instanceof Float || value instanceof BigDecimal) {
            return Numbers.toString(((Number) value).doubleValue());
        }
        return String.valueOf(value);
    }
 
    /**
     * 数值化:非数值返回 NaN,由调用方决定如何处理。
     */
    public static double toNumber(Object value) {
        if (value instanceof Number number) {
            return number.doubleValue();
        }
        if (value instanceof Boolean bool) {
            return bool ? 1D : 0D;
        }
        if (value instanceof String text && !text.isBlank()) {
            try {
                return Double.parseDouble(text.trim());
            } catch (NumberFormatException ignored) {
                return Double.NaN;
            }
        }
        return Double.NaN;
    }
 
    /**
     * 把任意取值收敛成数值数组,供统计函数使用。
     */
    public static List<Double> toNumberArray(Object value) {
        List<Double> numbers = new ArrayList<>();
        if (value instanceof Collection<?> collection) {
            flattenInto(collection, numbers);
            return numbers;
        }
        double single = toNumber(value);
        if (Double.isFinite(single)) {
            numbers.add(single);
        }
        return numbers;
    }
 
    private static void flattenInto(Object value, List<Double> target) {
        if (value instanceof Collection<?> collection) {
            for (Object element : collection) {
                flattenInto(element, target);
            }
            return;
        }
        double numeric = toNumber(value);
        if (Double.isFinite(numeric)) {
            target.add(numeric);
        }
    }
 
    /**
     * 把任意取值按一层一层摊平成列表,保留原始元素(不做数值转换),供 PASS_RATE 这类函数使用。
     */
    public static List<Object> flatten(Object value) {
        List<Object> result = new ArrayList<>();
        if (value instanceof Collection<?> collection) {
            for (Object element : collection) {
                result.addAll(flatten(element));
            }
            return result;
        }
        result.add(value);
        return result;
    }
 
}