View Javadoc
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 }