1 /*
2 * Copyright 2001-2005 Stephen Colebourne
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 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package org.joda.time.format;
17
18 import java.io.IOException;
19 import java.io.Writer;
20 import java.util.Locale;
21
22 import org.joda.time.MutablePeriod;
23 import org.joda.time.Period;
24 import org.joda.time.PeriodType;
25 import org.joda.time.ReadWritablePeriod;
26 import org.joda.time.ReadablePeriod;
27
28 /**
29 * Controls the printing and parsing of a time period to and from a string.
30 * <p>
31 * This class is the main API for printing and parsing used by most applications.
32 * Instances of this class are created via one of three factory classes:
33 * <ul>
34 * <li>{@link PeriodFormat} - formats by pattern and style</li>
35 * <li>{@link ISOPeriodFormat} - ISO8601 formats</li>
36 * <li>{@link PeriodFormatterBuilder} - complex formats created via method calls</li>
37 * </ul>
38 * <p>
39 * An instance of this class holds a reference internally to one printer and
40 * one parser. It is possible that one of these may be null, in which case the
41 * formatter cannot print/parse. This can be checked via the {@link #isPrinter()}
42 * and {@link #isParser()} methods.
43 * <p>
44 * The underlying printer/parser can be altered to behave exactly as required
45 * by using a decorator modifier:
46 * <ul>
47 * <li>{@link #withLocale(Locale)} - returns a new formatter that uses the specified locale</li>
48 * </ul>
49 * This returns a new formatter (instances of this class are immutable).
50 * <p>
51 * The main methods of the class are the <code>printXxx</code> and
52 * <code>parseXxx</code> methods. These are used as follows:
53 * <pre>
54 * // print using the default locale
55 * String periodStr = formatter.print(period);
56 * // print using the French locale
57 * String periodStr = formatter.withLocale(Locale.FRENCH).print(period);
58 *
59 * // parse using the French locale
60 * Period date = formatter.withLocale(Locale.FRENCH).parsePeriod(str);
61 * </pre>
62 *
63 * @author Brian S O'Neill
64 * @author Stephen Colebourne
65 * @since 1.0
66 */
67 public class PeriodFormatter {
68
69 /** The internal printer used to output the datetime. */
70 private final PeriodPrinter iPrinter;
71 /** The internal parser used to output the datetime. */
72 private final PeriodParser iParser;
73 /** The locale to use for printing and parsing. */
74 private final Locale iLocale;
75 /** The period type used in parsing. */
76 private final PeriodType iParseType;
77
78 /**
79 * Creates a new formatter, however you will normally use the factory
80 * or the builder.
81 *
82 * @param printer the internal printer, null if cannot print
83 * @param parser the internal parser, null if cannot parse
84 */
85 public PeriodFormatter(
86 PeriodPrinter printer, PeriodParser parser) {
87 super();
88 iPrinter = printer;
89 iParser = parser;
90 iLocale = null;
91 iParseType = null;
92 }
93
94 /**
95 * Constructor.
96 *
97 * @param printer the internal printer, null if cannot print
98 * @param parser the internal parser, null if cannot parse
99 * @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 }