1 /*
2 * Copyright (c) 2002-2026 Gargoyle Software Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 * https://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15 package org.htmlunit.html.parser;
16
17 import java.math.BigDecimal;
18
19 /**
20 * Utility methods for parsing HTML floating-point numbers.
21 * <p>
22 * This parser implements the syntax defined by the HTML Standard for
23 * <em>valid floating-point numbers</em>, which is used by elements such as
24 * {@code <input type="number">}. The accepted syntax differs from both
25 * Java number literals and JavaScript numeric literals.
26 * </p>
27 * <p>
28 * In particular:
29 * </p>
30 * <ul>
31 * <li>only ASCII digits ({@code 0-9}) are accepted,</li>
32 * <li>hexadecimal, binary, and octal notation are not supported,</li>
33 * <li>{@code NaN} and {@code Infinity} are rejected, and</li>
34 * <li>leading or trailing whitespace is not permitted.</li>
35 * </ul>
36 *
37 * @author Ronald Brill
38 */
39 public final class HtmlNumberParser {
40
41 /**
42 * Utility class; no instances.
43 */
44 private HtmlNumberParser() {
45 }
46
47 /**
48 * Returns whether the supplied string is a valid HTML floating-point number.
49 * <p>
50 * This method validates the complete input according to the HTML floating-point
51 * number grammar. A value is considered valid only if it can be parsed
52 * completely; partial matches are rejected.
53 * </p>
54 *
55 * @param value the string to validate
56 * @param acceptLeadingPlus set this to true to accept strings like "+7"
57 * @param acceptDotAtEnd set this to true to accept strings like "1."
58 * @return {@code true} if the supplied string is a valid HTML floating-point
59 * number; {@code false} otherwise
60 * @see #parse(String, boolean, boolean)
61 */
62 public static boolean isValid(final String value, final boolean acceptLeadingPlus, final boolean acceptDotAtEnd) {
63 return parse(value, acceptLeadingPlus, acceptDotAtEnd) != null;
64 }
65
66 /**
67 * Parses the supplied string as an HTML floating-point number.
68 * <p>
69 * If the input is syntactically valid, its numeric value is returned as a
70 * {@link BigDecimal}. Otherwise {@code null} is returned.
71 * </p>
72 * <p>
73 * This method performs syntax validation only. It does not apply any
74 * additional constraints that may be imposed by individual HTML algorithms
75 * or form controls.
76 * </p>
77 *
78 * @param value the string to parse
79 * @param acceptLeadingPlus set this to true to accept strings like "+7"
80 * @param acceptDotAtEnd set this to true to accept strings like "1."
81 * @return the parsed value, or {@code null} if the supplied string is not a
82 * valid HTML floating-point number
83 */
84 public static BigDecimal parse(final String value, final boolean acceptLeadingPlus, final boolean acceptDotAtEnd) {
85 if (value == null || value.isEmpty()) {
86 return null;
87 }
88
89 int pos = 0;
90 final int len = value.length();
91
92 final StringBuilder normalized = new StringBuilder(len + 5);
93
94 // sign
95 final char sign = value.charAt(pos);
96 if (sign == '-') {
97 normalized.append(sign);
98 pos++;
99 if (pos == len) {
100 return null;
101 }
102 }
103 else if (acceptLeadingPlus && sign == '+') {
104 normalized.append(sign);
105 pos++;
106 if (pos == len) {
107 return null;
108 }
109 }
110
111 boolean digits = false;
112
113 while (pos < len) {
114 final char ch = value.charAt(pos);
115 if (ch >= '0' && ch <= '9') {
116 digits = true;
117 normalized.append(ch);
118 pos++;
119 }
120 else {
121 break;
122 }
123 }
124
125 if (pos < len && value.charAt(pos) == '.') {
126 normalized.append('.');
127 pos++;
128
129 while (pos < len) {
130 final char ch = value.charAt(pos);
131 if (ch >= '0' && ch <= '9') {
132 digits = true;
133 normalized.append(ch);
134 pos++;
135 }
136 else {
137 break;
138 }
139 }
140 }
141
142 if (!digits) {
143 return null;
144 }
145
146 if (pos < len && (value.charAt(pos) == 'e' || value.charAt(pos) == 'E')) {
147 normalized.append('E');
148 pos++;
149
150 if (pos == len) {
151 return null;
152 }
153
154 if (value.charAt(pos) == '+' || value.charAt(pos) == '-') {
155 normalized.append(value.charAt(pos++));
156 if (pos == len) {
157 return null;
158 }
159 }
160
161 boolean exponentDigits = false;
162
163 while (pos < len) {
164 final char ch = value.charAt(pos);
165 if (ch >= '0' && ch <= '9') {
166 exponentDigits = true;
167 normalized.append(ch);
168 pos++;
169 }
170 else {
171 break;
172 }
173 }
174
175 if (!exponentDigits) {
176 return null;
177 }
178 }
179
180 if (pos != len) {
181 return null;
182 }
183
184 if (!acceptDotAtEnd
185 && pos > 0 && value.charAt(pos - 1) == '.') {
186 return null;
187 }
188
189 try {
190 return new BigDecimal(normalized.toString());
191 }
192 catch (final NumberFormatException e) {
193 return null;
194 }
195 }
196 }