001/*
002 *  Copyright 2001-2005 Stephen Colebourne
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.joda.time.format;
017
018import java.io.IOException;
019import java.io.Writer;
020import java.util.Locale;
021
022import org.joda.time.MutablePeriod;
023import org.joda.time.Period;
024import org.joda.time.PeriodType;
025import org.joda.time.ReadWritablePeriod;
026import org.joda.time.ReadablePeriod;
027
028/**
029 * Controls the printing and parsing of a time period to and from a string.
030 * <p>
031 * This class is the main API for printing and parsing used by most applications.
032 * Instances of this class are created via one of three factory classes:
033 * <ul>
034 * <li>{@link PeriodFormat} - formats by pattern and style</li>
035 * <li>{@link ISOPeriodFormat} - ISO8601 formats</li>
036 * <li>{@link PeriodFormatterBuilder} - complex formats created via method calls</li>
037 * </ul>
038 * <p>
039 * An instance of this class holds a reference internally to one printer and
040 * one parser. It is possible that one of these may be null, in which case the
041 * formatter cannot print/parse. This can be checked via the {@link #isPrinter()}
042 * and {@link #isParser()} methods.
043 * <p>
044 * The underlying printer/parser can be altered to behave exactly as required
045 * by using a decorator modifier:
046 * <ul>
047 * <li>{@link #withLocale(Locale)} - returns a new formatter that uses the specified locale</li>
048 * </ul>
049 * This returns a new formatter (instances of this class are immutable).
050 * <p>
051 * The main methods of the class are the <code>printXxx</code> and
052 * <code>parseXxx</code> methods. These are used as follows:
053 * <pre>
054 * // print using the default locale
055 * String periodStr = formatter.print(period);
056 * // print using the French locale
057 * String periodStr = formatter.withLocale(Locale.FRENCH).print(period);
058 * 
059 * // parse using the French locale
060 * Period date = formatter.withLocale(Locale.FRENCH).parsePeriod(str);
061 * </pre>
062 *
063 * @author Brian S O'Neill
064 * @author Stephen Colebourne
065 * @since 1.0
066 */
067public class PeriodFormatter {
068
069    /** The internal printer used to output the datetime. */
070    private final PeriodPrinter iPrinter;
071    /** The internal parser used to output the datetime. */
072    private final PeriodParser iParser;
073    /** The locale to use for printing and parsing. */
074    private final Locale iLocale;
075    /** The period type used in parsing. */
076    private final PeriodType iParseType;
077
078    /**
079     * Creates a new formatter, however you will normally use the factory
080     * or the builder.
081     * 
082     * @param printer  the internal printer, null if cannot print
083     * @param parser  the internal parser, null if cannot parse
084     */
085    public PeriodFormatter(
086            PeriodPrinter printer, PeriodParser parser) {
087        super();
088        iPrinter = printer;
089        iParser = parser;
090        iLocale = null;
091        iParseType = null;
092    }
093
094    /**
095     * Constructor.
096     * 
097     * @param printer  the internal printer, null if cannot print
098     * @param parser  the internal parser, null if cannot parse
099     * @param locale  the locale to use
100     * @param type  the parse period type
101     */
102    private PeriodFormatter(
103            PeriodPrinter printer, PeriodParser parser,
104            Locale locale, PeriodType type) {
105        super();
106        iPrinter = printer;
107        iParser = parser;
108        iLocale = locale;
109        iParseType = type;
110    }
111
112    //-----------------------------------------------------------------------
113    /**
114     * Is this formatter capable of printing.
115     * 
116     * @return true if this is a printer
117     */
118    public boolean isPrinter() {
119        return (iPrinter != null);
120    }
121
122    /**
123     * Gets the internal printer object that performs the real printing work.
124     * 
125     * @return the internal printer
126     */
127    public PeriodPrinter getPrinter() {
128        return iPrinter;
129    }
130
131    /**
132     * Is this formatter capable of parsing.
133     * 
134     * @return true if this is a parser
135     */
136    public boolean isParser() {
137        return (iParser != null);
138    }
139
140    /**
141     * Gets the internal parser object that performs the real parsing work.
142     * 
143     * @return the internal parser
144     */
145    public PeriodParser getParser() {
146        return iParser;
147    }
148
149    //-----------------------------------------------------------------------
150    /**
151     * Returns a new formatter with a different locale that will be used
152     * for printing and parsing.
153     * <p>
154     * A PeriodFormatter is immutable, so a new instance is returned,
155     * and the original is unaltered and still usable.
156     * 
157     * @param locale  the locale to use
158     * @return the new formatter
159     */
160    public PeriodFormatter withLocale(Locale locale) {
161        if (locale == getLocale() || (locale != null && locale.equals(getLocale()))) {
162            return this;
163        }
164        return new PeriodFormatter(iPrinter, iParser, locale, iParseType);
165    }
166
167    /**
168     * Gets the locale that will be used for printing and parsing.
169     * 
170     * @return the locale to use
171     */
172    public Locale getLocale() {
173        return iLocale;
174    }
175
176    //-----------------------------------------------------------------------
177    /**
178     * Returns a new formatter with a different PeriodType for parsing.
179     * <p>
180     * A PeriodFormatter is immutable, so a new instance is returned,
181     * and the original is unaltered and still usable.
182     * 
183     * @param type  the type to use in parsing
184     * @return the new formatter
185     */
186    public PeriodFormatter withParseType(PeriodType type) {
187        if (type == iParseType) {
188            return this;
189        }
190        return new PeriodFormatter(iPrinter, iParser, iLocale, type);
191    }
192
193    /**
194     * Gets the PeriodType that will be used for parsing.
195     * 
196     * @return the parse type to use
197     */
198    public PeriodType getParseType() {
199        return iParseType;
200    }
201
202    //-----------------------------------------------------------------------
203    /**
204     * Prints a ReadablePeriod to a StringBuffer.
205     *
206     * @param buf  the formatted period is appended to this buffer
207     * @param period  the period to format, not null
208     */
209    public void printTo(StringBuffer buf, ReadablePeriod period) {
210        checkPrinter();
211        checkPeriod(period);
212        
213        getPrinter().printTo(buf, period, iLocale);
214    }
215
216    /**
217     * Prints a ReadablePeriod to a Writer.
218     *
219     * @param out  the formatted period is written out
220     * @param period  the period to format, not null
221     */
222    public void printTo(Writer out, ReadablePeriod period) throws IOException {
223        checkPrinter();
224        checkPeriod(period);
225        
226        getPrinter().printTo(out, period, iLocale);
227    }
228
229    /**
230     * Prints a ReadablePeriod to a new String.
231     *
232     * @param period  the period to format, not null
233     * @return the printed result
234     */
235    public String print(ReadablePeriod period) {
236        checkPrinter();
237        checkPeriod(period);
238        
239        PeriodPrinter printer = getPrinter();
240        StringBuffer buf = new StringBuffer(printer.calculatePrintedLength(period, iLocale));
241        printer.printTo(buf, period, iLocale);
242        return buf.toString();
243    }
244
245    /**
246     * Checks whether printing is supported.
247     * 
248     * @throws UnsupportedOperationException if printing is not supported
249     */
250    private void checkPrinter() {
251        if (iPrinter == null) {
252            throw new UnsupportedOperationException("Printing not supported");
253        }
254    }
255
256    /**
257     * Checks whether the period is non-null.
258     * 
259     * @throws IllegalArgumentException if the period is null
260     */
261    private void checkPeriod(ReadablePeriod period) {
262        if (period == null) {
263            throw new IllegalArgumentException("Period must not be null");
264        }
265    }
266
267    //-----------------------------------------------------------------------
268    /**
269     * Parses a period from the given text, at the given position, saving the
270     * result into the fields of the given ReadWritablePeriod. If the parse
271     * succeeds, the return value is the new text position. Note that the parse
272     * may succeed without fully reading the text.
273     * <p>
274     * The parse type of the formatter is not used by this method.
275     * <p>
276     * If it fails, the return value is negative, but the period may still be
277     * modified. To determine the position where the parse failed, apply the
278     * one's complement operator (~) on the return value.
279     *
280     * @param period  a period that will be modified
281     * @param text  text to parse
282     * @param position position to start parsing from
283     * @return new position, if negative, parse failed. Apply complement
284     * operator (~) to get position of failure
285     * @throws IllegalArgumentException if any field is out of range
286     */
287    public int parseInto(ReadWritablePeriod period, String text, int position) {
288        checkParser();
289        checkPeriod(period);
290        
291        return getParser().parseInto(period, text, position, iLocale);
292    }
293
294    /**
295     * Parses a period from the given text, returning a new Period.
296     *
297     * @param text  text to parse
298     * @return parsed value in a Period object
299     * @throws IllegalArgumentException if any field is out of range
300     */
301    public Period parsePeriod(String text) {
302        checkParser();
303        
304        return parseMutablePeriod(text).toPeriod();
305    }
306
307    /**
308     * Parses a period from the given text, returning a new MutablePeriod.
309     *
310     * @param text  text to parse
311     * @return parsed value in a MutablePeriod object
312     * @throws IllegalArgumentException if any field is out of range
313     */
314    public MutablePeriod parseMutablePeriod(String text) {
315        checkParser();
316        
317        MutablePeriod period = new MutablePeriod(0, iParseType);
318        int newPos = getParser().parseInto(period, text, 0, iLocale);
319        if (newPos >= 0) {
320            if (newPos >= text.length()) {
321                return period;
322            }
323        } else {
324            newPos = ~newPos;
325        }
326        throw new IllegalArgumentException(FormatUtils.createErrorMessage(text, newPos));
327    }
328
329    /**
330     * Checks whether parsing is supported.
331     * 
332     * @throws UnsupportedOperationException if parsing is not supported
333     */
334    private void checkParser() {
335        if (iParser == null) {
336            throw new UnsupportedOperationException("Parsing not supported");
337        }
338    }
339
340}