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}