001/* 002 * Units of Measurement Reference Implementation 003 * Copyright (c) 2005-2025, Jean-Marie Dautelle, Werner Keil, Otavio Santana. 004 * 005 * All rights reserved. 006 * 007 * Redistribution and use in source and binary forms, with or without modification, 008 * are permitted provided that the following conditions are met: 009 * 010 * 1. Redistributions of source code must retain the above copyright notice, 011 * this list of conditions and the following disclaimer. 012 * 013 * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions 014 * and the following disclaimer in the documentation and/or other materials provided with the distribution. 015 * 016 * 3. Neither the name of JSR-385, Indriya nor the names of their contributors may be used to endorse or promote products 017 * derived from this software without specific prior written permission. 018 * 019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 020 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 021 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 022 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 023 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED 026 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 028 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 029 */ 030package tech.units.indriya.format; 031 032import static tech.units.indriya.format.CommonFormatter.parseMixedAsLeading; 033 034import java.io.IOException; 035import java.text.NumberFormat; 036import java.text.ParsePosition; 037import java.util.Objects; 038 039import javax.measure.Quantity; 040import javax.measure.Unit; 041import javax.measure.format.MeasurementParseException; 042 043import tech.units.indriya.AbstractUnit; 044import tech.units.indriya.internal.format.RationalNumberScanner; 045import tech.units.indriya.quantity.MixedQuantity; 046import tech.units.indriya.quantity.Quantities; 047import tech.units.indriya.spi.Range; 048 049/** 050 * A simple implementation of {@link QuantityFormat} 051 * 052 * <br> 053 * The following pattern letters are defined: 054 * <blockquote> 055 * <table class="striped"> 056 * <caption style="display:none">Chart shows pattern letters, date/time component, presentation, and examples.</caption> 057 * <thead> 058 * <tr> 059 * <th style="text-align:left">Letter 060 * <th style="text-align:left">Quantity Component 061 * <th style="text-align:left">Presentation 062 * <th style="text-align:left">Examples 063 * </thead> 064 * <tbody> 065 * <tr> 066 * <td><code>n</code> 067 * <td>Numeric value 068 * <td><a href="#number">Number</a> 069 * <td><code>27</code> 070 * <tr> 071 * <td><code>u</code> 072 * <td>Unit 073 * <td><a href="#text">Text</a> 074 * <td><code>m</code> 075 * <tr> 076 * <td><code>~</code> 077 * <td>Mixed radix 078 * <td><a href="#text">Text</a> 079 * <td><code>1 m</code>; 27 <code>cm</code> 080<tr> 081 * <td><code>rc</code> 082 * <td>Range 083 * <td><a href="#text">Compact</a> 084 * <td><code>min=1 m, max=5 m</code> 085 * </tbody> 086 * </table> 087 * </blockquote> 088 * Pattern letters are usually repeated, as their number determines the 089 * exact presentation: 090 * <ul> 091 * <li><strong><a id="text">Text:</a></strong> 092 * For formatting, if the number of pattern letters is 4 or more, 093 * the full form is used; otherwise a short or abbreviated form 094 * is used if available. 095 * For parsing, both forms are accepted, independent of the number 096 * of pattern letters.<br><br></li> 097 * <li><strong><a id="number">Number:</a></strong> 098 * For formatting, the number of pattern letters is the minimum 099 * number of digits, and shorter numbers are zero-padded to this amount. 100 * For parsing, the number of pattern letters is ignored unless 101 * it's needed to separate two adjacent fields.<br><br></li> 102 * 103 *<li><strong><a id="radix">Mixed Radix:</a></strong> 104 * The Mixed radix marker <code>"~"</code> is followed by a character sequence acting as mixed radix delimiter. This character sequence must not contain <code>"~"</code> itself or any numeric values.<br><br></li> 105 * 106 *<li><strong><a id="radix">Range:</a></strong> 107 * The Range compact part <code>"rc"</code> only applies to formatting instances of {@link Range} via <code>formatRange()</code>. It may be combined with the others. If set alone, then the default number and unit formatting is assumed.<br></li> 108 * </ul> 109 * @version 2.3.2, Apr 23, 2025 110 * @since 2.0 111 */ 112@SuppressWarnings("rawtypes") 113public class SimpleQuantityFormat extends AbstractQuantityFormat { 114 /** 115 * Holds the default format instance. 116 */ 117 private static final SimpleQuantityFormat DEFAULT = new SimpleQuantityFormat(); 118 119 private static final String NUM_PART = "n"; 120 private static final String UNIT_PART = "u"; 121 private static final String RADIX = "~"; 122 private static final String RANGE_COMPACT = "rc"; 123 124 private static final String DEFAULT_PATTERN = "n u"; 125 126 /** 127 * The pattern string of this formatter. This is always a non-localized pattern. 128 * May not be null. See class documentation for details. 129 * 130 * @serial 131 */ 132 private final String pattern; 133 134 private String delimiter; 135 136 private String mixDelimiter; 137 138 private final boolean rangeCompact; 139 140 /** 141 * 142 */ 143 private static final long serialVersionUID = 2758248665095734058L; 144 145 /** 146 * Constructs a <code>SimpleQuantityFormat</code> using the given pattern. 147 * <p> 148 * 149 * @param pattern 150 * the pattern describing the quantity and unit format 151 * @exception NullPointerException 152 * if the given pattern is null 153 * @exception IllegalArgumentException 154 * if the given pattern is empty or invalid 155 */ 156 public SimpleQuantityFormat(String pattern) { 157 Objects.requireNonNull(pattern); 158 if (pattern != null && !pattern.isEmpty()) { 159 if (RANGE_COMPACT.equals(pattern)) { 160 rangeCompact = true; 161 this.pattern = DEFAULT_PATTERN; 162 } else if (pattern.contains(RANGE_COMPACT)) { 163 this.pattern = pattern; 164 rangeCompact = true; 165 } else { 166 this.pattern = pattern; 167 rangeCompact = false; 168 } 169 if (this.pattern.contains(RADIX)) { 170 final String singlePattern = this.pattern.substring(0, this.pattern.indexOf(RADIX)); 171 mixDelimiter = this.pattern.substring(this.pattern.indexOf(RADIX) + 1); 172 delimiter = singlePattern.substring(this.pattern.indexOf(NUM_PART)+1, this.pattern.indexOf(UNIT_PART)); 173 } else { 174 delimiter = this.pattern.substring(this.pattern.indexOf(NUM_PART)+1, this.pattern.indexOf(UNIT_PART)); 175 } 176 } else { 177 throw new IllegalArgumentException("Pattern cannot be empty"); 178 } 179 } 180 181 /** 182 * Constructs a <code>SimpleQuantityFormat</code> using the default pattern. For 183 * full coverage, use the factory methods. 184 */ 185 protected SimpleQuantityFormat() { 186 this(DEFAULT_PATTERN); 187 } 188 189 @Override 190 public Appendable format(Quantity<?> quantity, Appendable dest) throws IOException { 191 final Unit unit = quantity.getUnit(); 192 /* 193 if (unit instanceof MixedUnit) { 194 if (quantity instanceof MixedQuantity) { 195 final MixedQuantity<?> compQuant = (MixedQuantity<?>) quantity; 196 final MixedUnit<?> compUnit = (MixedUnit<?>) unit; 197 final Number[] values = compQuant.getValues(); 198 if (values.length == compUnit.getUnits().size()) { 199 final StringBuffer sb = new StringBuffer(); // we use StringBuffer here because of java.text.Format compatibility 200 for (int i = 0; i < values.length; i++) { 201 sb.append(SimpleQuantityFormat.getInstance().format( 202 Quantities.getQuantity(values[i], compUnit.getUnits().get(i), compQuant.getScale()))); 203 if (i < values.length-1) { 204 sb.append(delimiter); 205 } 206 } 207 return sb; 208 } else { 209 throw new IllegalArgumentException(String.format("%s values don't match %s in mixed unit", values.length, compUnit.getUnits().size())); 210 } 211 } else { 212 throw new MeasurementException("The quantity is not a mixed quantity"); 213 } 214 } else { */ 215 dest.append(quantity.getValue().toString()); 216 if (quantity.getUnit().equals(AbstractUnit.ONE)) 217 return dest; 218 dest.append(delimiter); 219 return SimpleUnitFormat.getInstance().format(unit, dest); 220 //} 221 } 222 223 /** 224 * Formats a {@link Range}.<br> 225 * If the special pattern part "rc" is applied, the compact format like "min=", "max=" is used, 226 * otherwise the full words like "minimum", "maximum", "resolution". 227 * @param range 228 * the range to format. 229 * @return the formatted range. 230 * @since 2.3 231 */ 232 public String formatRange(Range<?> range) { 233 final StringBuilder sb = new StringBuilder().append(rangeCompact ? "min=" : "minimum=") 234 .append(range.getMinimum()).append(rangeCompact ? ", max=" : ", maximum=") 235 .append(range.getMaximum()); 236 if (range.getResolution() != null) { 237 sb.append(rangeCompact ? ", res=" : ", resolution=").append(range.getResolution()); 238 } 239 return sb.toString(); 240 } 241 242 @SuppressWarnings("unchecked") 243 @Override 244 public Quantity<?> parse(CharSequence csq, ParsePosition cursor) throws MeasurementParseException { 245 246 final NumberFormat numberFormat = NumberFormat.getInstance(); 247 final SimpleUnitFormat simpleUnitFormat = SimpleUnitFormat.getInstance(); 248 249 if (mixDelimiter != null && !mixDelimiter.equals(delimiter)) { 250 return parseMixedAsLeading(csq.toString(), numberFormat, simpleUnitFormat, delimiter, mixDelimiter, cursor.getIndex()); 251 } else if (mixDelimiter != null && mixDelimiter.equals(delimiter)) { 252 return parseMixedAsLeading(csq.toString(), numberFormat, simpleUnitFormat, delimiter, cursor.getIndex()); 253 } 254 255 final RationalNumberScanner scanner = new RationalNumberScanner(csq, cursor, null /*TODO should'nt this be numberFormat as well*/); 256 final Number number = scanner.getNumber(); 257 258 Unit unit = simpleUnitFormat.parse(csq, cursor); 259 return Quantities.getQuantity(number, unit); 260 } 261 262 @Override 263 protected Quantity<?> parse(CharSequence csq, int index) throws MeasurementParseException { 264 return parse(csq, new ParsePosition(index)); 265 } 266 267 @Override 268 public Quantity<?> parse(CharSequence csq) throws MeasurementParseException { 269 return parse(csq, new ParsePosition(0)); 270 } 271 272 /** 273 * Returns the quantity format for the default locale. The default format 274 * assumes the quantity is composed of a decimal number and a {@link Unit} 275 * separated by whitespace(s). 276 * 277 * @return a default <code>SimpleQuantityFormat</code> instance. 278 */ 279 public static SimpleQuantityFormat getInstance() { 280 return DEFAULT; 281 } 282 283 /** 284 * Returns a <code>SimpleQuantityFormat</code> using the given pattern. 285 * <p> 286 * 287 * @param pattern 288 * the pattern describing the quantity and unit format 289 * 290 * @return <code>SimpleQuantityFormat.getInstance(a pattern)</code> 291 */ 292 public static SimpleQuantityFormat getInstance(String pattern) { 293 return new SimpleQuantityFormat(pattern); 294 } 295 296 @Override 297 public String toString() { 298 return getClass().getSimpleName(); 299 } 300 301 /** 302 * Returns the pattern of this format. 303 * <p> 304 * 305 * @return a <code>pattern</code> 306 */ 307 public String getPattern() { 308 return pattern; 309 } 310 311 @Override 312 protected StringBuffer formatMixed(MixedQuantity<?> mixed, StringBuffer dest) { 313 final StringBuffer sb = new StringBuffer(); 314 int i = 0; 315 for (Quantity<?> q : mixed.getQuantities()) { 316 sb.append(format(q)); 317 if (i < mixed.getQuantities().size() - 1 ) { 318 sb.append((mixDelimiter != null ? mixDelimiter : DEFAULT_DELIMITER)); // we need null for parsing but not 319 } 320 i++; 321 } 322 return sb; 323 } 324}