001/*
002 * Units of Measurement Reference Implementation
003 * Copyright (c) 2005-2023, 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;
037
038import javax.measure.Quantity;
039import javax.measure.Unit;
040import javax.measure.format.MeasurementParseException;
041
042import tech.units.indriya.AbstractUnit;
043import tech.units.indriya.internal.format.RationalNumberScanner;
044import tech.units.indriya.quantity.MixedQuantity;
045import tech.units.indriya.quantity.Quantities;
046
047/**
048 * A simple implementation of QuantityFormat
049 * 
050 * <br>
051 * The following pattern letters are defined:
052 * <blockquote>
053 * <table class="striped">
054 * <caption style="display:none">Chart shows pattern letters, date/time component, presentation, and examples.</caption>
055 * <thead>
056 *     <tr>
057 *         <th style="text-align:left">Letter
058 *         <th style="text-align:left">Quantity Component
059 *         <th style="text-align:left">Presentation
060 *         <th style="text-align:left">Examples
061 * </thead>
062 * <tbody>
063 *     <tr>
064 *         <td><code>n</code>
065 *         <td>Numeric value
066 *         <td><a href="#number">Number</a>
067 *         <td><code>27</code>
068 *     <tr>
069 *         <td><code>u</code>
070 *         <td>Unit
071 *         <td><a href="#text">Text</a>
072 *         <td><code>m</code>
073 *    <tr>
074 *         <td><code>~</code>
075 *         <td>Mixed radix
076 *         <td><a href="#text">Text</a>
077 *         <td><code>1 m</code>; 27 <code>cm</code>
078 * </tbody>
079 * </table>
080 * </blockquote>
081 * Pattern letters are usually repeated, as their number determines the
082 * exact presentation:
083 * <ul>
084 * <li><strong><a id="text">Text:</a></strong>
085 *     For formatting, if the number of pattern letters is 4 or more,
086 *     the full form is used; otherwise a short or abbreviated form
087 *     is used if available.
088 *     For parsing, both forms are accepted, independent of the number
089 *     of pattern letters.<br><br></li>
090 * <li><strong><a id="number">Number:</a></strong>
091 *     For formatting, the number of pattern letters is the minimum
092 *     number of digits, and shorter numbers are zero-padded to this amount.
093 *     For parsing, the number of pattern letters is ignored unless
094 *     it's needed to separate two adjacent fields.<br><br></li>
095 *     
096 *<li><strong><a id="radix">Mixed Radix:</a></strong>
097 *     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></li>
098 * </ul> 
099 * @version 2.1, June 5, 2023
100 * @since 2.0
101 */
102@SuppressWarnings("rawtypes")
103public class SimpleQuantityFormat extends AbstractQuantityFormat {
104        /**
105         * Holds the default format instance.
106         */
107        private static final SimpleQuantityFormat DEFAULT = new SimpleQuantityFormat();
108
109        private static final String NUM_PART = "n";
110        private static final String UNIT_PART = "u";
111        private static final String RADIX = "~";
112        
113        /**
114         * The pattern string of this formatter. This is always a non-localized pattern.
115         * May not be null. See class documentation for details.
116         * 
117         * @serial
118         */
119        private final String pattern;
120        
121        private String delimiter;
122        
123        private String mixDelimiter;
124
125        /**
126         *
127         */
128        private static final long serialVersionUID = 2758248665095734058L;
129
130        /**
131         * Constructs a <code>SimpleQuantityFormat</code> using the given pattern.
132         * <p>
133         * 
134         * @param pattern
135         *            the pattern describing the quantity and unit format
136         * @exception NullPointerException
137         *                if the given pattern is null
138         * @exception IllegalArgumentException
139         *                if the given pattern is invalid
140         */
141        public SimpleQuantityFormat(String pattern) {
142                this.pattern = pattern;
143                if (pattern != null && !pattern.isEmpty()) {
144                   if (pattern.contains(RADIX)) {
145                       final String singlePattern = pattern.substring(0, pattern.indexOf(RADIX));
146                       mixDelimiter = pattern.substring(pattern.indexOf(RADIX) + 1);
147                       delimiter = singlePattern.substring(pattern.indexOf(NUM_PART)+1, pattern.indexOf(UNIT_PART));
148                   } else {
149                       delimiter = pattern.substring(pattern.indexOf(NUM_PART)+1, pattern.indexOf(UNIT_PART));
150                   }
151                }
152        }
153
154        /**
155         * Constructs a <code>SimpleQuantityFormat</code> using the default pattern. For
156         * full coverage, use the factory methods.
157         */
158        protected SimpleQuantityFormat() {
159                this("n u");
160        }
161
162        @Override
163        public Appendable format(Quantity<?> quantity, Appendable dest) throws IOException {
164                final Unit unit = quantity.getUnit();
165        /*
166                if (unit instanceof MixedUnit) {
167            if (quantity instanceof MixedQuantity) {
168                final MixedQuantity<?> compQuant = (MixedQuantity<?>) quantity;
169                final MixedUnit<?> compUnit = (MixedUnit<?>) unit;
170                final Number[] values = compQuant.getValues();
171                if (values.length == compUnit.getUnits().size()) {
172                    final StringBuffer sb = new StringBuffer(); // we use StringBuffer here because of java.text.Format compatibility
173                    for (int i = 0; i < values.length; i++) {
174                       sb.append(SimpleQuantityFormat.getInstance().format(
175                               Quantities.getQuantity(values[i], compUnit.getUnits().get(i), compQuant.getScale())));
176                       if (i < values.length-1) {
177                           sb.append(delimiter);
178                       }
179                    }
180                    return sb;
181                } else {
182                    throw new IllegalArgumentException(String.format("%s values don't match %s in mixed unit", values.length, compUnit.getUnits().size()));
183                }
184            } else {
185                throw new MeasurementException("The quantity is not a mixed quantity");
186            }
187        } else { */
188                dest.append(quantity.getValue().toString());
189                if (quantity.getUnit().equals(AbstractUnit.ONE))
190                        return dest;
191                dest.append(delimiter);
192                return SimpleUnitFormat.getInstance().format(unit, dest);
193        //}
194        }
195        
196        @SuppressWarnings("unchecked")
197        @Override
198        public Quantity<?> parse(CharSequence csq, ParsePosition cursor) throws MeasurementParseException {
199            
200            final NumberFormat numberFormat = NumberFormat.getInstance();
201            final SimpleUnitFormat simpleUnitFormat = SimpleUnitFormat.getInstance();
202            
203        if (mixDelimiter != null && !mixDelimiter.equals(delimiter)) {
204            return parseMixedAsLeading(csq.toString(), numberFormat, simpleUnitFormat, delimiter, mixDelimiter, cursor.getIndex());
205        } else if (mixDelimiter != null && mixDelimiter.equals(delimiter)) {
206            return parseMixedAsLeading(csq.toString(), numberFormat, simpleUnitFormat, delimiter, cursor.getIndex());
207        }
208        
209        final RationalNumberScanner scanner = new RationalNumberScanner(csq, cursor, null /*TODO should'nt this be numberFormat as well*/);
210        final Number number = scanner.getNumber();
211                
212                Unit unit = simpleUnitFormat.parse(csq, cursor);
213                return Quantities.getQuantity(number, unit);
214        }
215
216        @Override
217        protected Quantity<?> parse(CharSequence csq, int index) throws MeasurementParseException {
218                return parse(csq, new ParsePosition(index));
219        }
220
221        @Override
222        public Quantity<?> parse(CharSequence csq) throws MeasurementParseException {
223                return parse(csq, new ParsePosition(0));
224        }
225
226        /**
227         * Returns the quantity format for the default locale. The default format
228         * assumes the quantity is composed of a decimal number and a {@link Unit}
229         * separated by whitespace(s).
230         *
231         * @return a default <code>SimpleQuantityFormat</code> instance.
232         */
233        public static SimpleQuantityFormat getInstance() {
234                return DEFAULT;
235        }
236        
237        /**
238         * Returns a <code>SimpleQuantityFormat</code> using the given pattern.
239         * <p>
240         * 
241         * @param pattern
242         *            the pattern describing the quantity and unit format
243         *
244         * @return <code>SimpleQuantityFormat.getInstance(a pattern)</code>
245         */
246        public static SimpleQuantityFormat getInstance(String pattern) {
247                return new SimpleQuantityFormat(pattern);
248        }
249
250        @Override
251        public String toString() {
252            return getClass().getSimpleName();
253        }
254          
255        public String getPattern() {
256                return pattern;
257        }
258        
259    @Override
260    protected StringBuffer formatMixed(MixedQuantity<?> mixed, StringBuffer dest) {
261        final StringBuffer sb = new StringBuffer();
262        int i = 0;
263        for (Quantity<?> q : mixed.getQuantities()) {
264            sb.append(format(q));
265            if (i < mixed.getQuantities().size() - 1 ) {
266                sb.append((mixDelimiter != null ? mixDelimiter : DEFAULT_DELIMITER)); // we need null for parsing but not
267            }
268            i++;
269        }
270        return sb;
271    }
272}