001/*
002 * Copyright 2006 - 2013
003 *     Stefan Balev     <stefan.balev@graphstream-project.org>
004 *     Julien Baudry    <julien.baudry@graphstream-project.org>
005 *     Antoine Dutot    <antoine.dutot@graphstream-project.org>
006 *     Yoann Pigné      <yoann.pigne@graphstream-project.org>
007 *     Guilhelm Savin   <guilhelm.savin@graphstream-project.org>
008 * 
009 * This file is part of GraphStream <http://graphstream-project.org>.
010 * 
011 * GraphStream is a library whose purpose is to handle static or dynamic
012 * graph, create them from scratch, file or any source and display them.
013 * 
014 * This program is free software distributed under the terms of two licenses, the
015 * CeCILL-C license that fits European law, and the GNU Lesser General Public
016 * License. You can  use, modify and/ or redistribute the software under the terms
017 * of the CeCILL-C license as circulated by CEA, CNRS and INRIA at the following
018 * URL <http://www.cecill.info> or under the terms of the GNU LGPL as published by
019 * the Free Software Foundation, either version 3 of the License, or (at your
020 * option) any later version.
021 * 
022 * This program is distributed in the hope that it will be useful, but WITHOUT ANY
023 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
024 * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
025 * 
026 * You should have received a copy of the GNU Lesser General Public License
027 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
028 * 
029 * The fact that you are presently reading this means that you have had
030 * knowledge of the CeCILL-C and LGPL licenses and that you accept their terms.
031 */
032package org.graphstream.util.time;
033
034import java.text.ParseException;
035import java.util.Calendar;
036import java.util.LinkedList;
037import java.util.regex.Matcher;
038import java.util.regex.Pattern;
039
040import org.graphstream.util.time.ISODateComponent.TextComponent;
041
042/**
043 * Scanner for date in ISO/IEC 9899:1999 format. The scanner takes a format and
044 * then is able to parse timestamp in the given format.
045 * 
046 * The <i>parse()</i> return a {@link java.util.Calendar} for convenience.
047 * 
048 * Format of the scanner can be composed of %? directive which define components
049 * of the time. These directives are listed below. For example, the format
050 * "%F %T", which is equivalent to "%Y-%m-%d %H:%M:%S" can parse the following
051 * timestamp: "2010-12-09 03:45:39";
052 * 
053 * <dl>
054 * <dt>%a</dt>
055 * <dd>locale's abbreviated weekday name</dd>
056 * <dt>%A</dt>
057 * <dd>locale's weekday name</dd>
058 * <dt>%b</dt>
059 * <dd>locale's abbreviated month name</dd>
060 * <dt>%B</dt>
061 * <dd>locale's month name</dd>
062 * <dt>%c</dt>
063 * <dd>locale's date and time representation</dd>
064 * <dt>%C</dt>
065 * <dd>two first digits of full year as an integer (00-99)</dd>
066 * <dt>%d</dt>
067 * <dd>day of the month (01-31)</dd>
068 * <dt>%D</dt>
069 * <dd>%m/%d/%y</dd>
070 * <dt>%e</dt>
071 * <dd>day of the month (1-31)</dd>
072 * <dt>%F</dt>
073 * <dd>%Y-%m-%d</dd>
074 * <dt>%g</dt>
075 * <dd>last 2 digits of the week-based year (00-99)</dd>
076 * <dt>%G</dt>
077 * <dd>"week-based year as a decimal number</dd>
078 * <dt>%h</dt>
079 * <dd>%b</dd>
080 * <dt>%H</dt>
081 * <dd>hour (24-hour clock) as a decimal number (00-23)</dd>
082 * <dt>%I</dt>
083 * <dd>hour (12-hour clock) as a decimal number (01-12)</dd>
084 * <dt>%j</dt>
085 * <dd>day of the year as a decimal number (001-366)</dd>
086 * <dt>%k</dt>
087 * <dd>milliseconds as a decimal number (001-999)</dd>
088 * <dt>%K</dt>
089 * <dd>milliseconds since the epoch</dd>
090 * <dt>%m</dt>
091 * <dd>month as a decimal number (01-12)</dd>
092 * <dt>%M</dt>
093 * <dd>minute as a decimal number (00-59)</dd>
094 * <dt>%n</dt>
095 * <dd>\n</dd>
096 * <dt>%p</dt>
097 * <dd>locale-s equivalent of the AM/PM</dd>
098 * <dt>%r</dt>
099 * <dd>locale's 12-hour clock time</dd>
100 * <dt>%R</dt>
101 * <dd>%H:%M</dd>
102 * <dt>%S</dt>
103 * <dd>second as a decimal number (00-60)</dd>
104 * <dt>%t</dt>
105 * <dd>\t</dd>
106 * <dt>%T</dt>
107 * <dd>%H:%M:%S</dd>
108 * <dt>%u</dt>
109 * <dd>ISO 8601 weekday as a decimal number (1-7)</dd>
110 * <dt>%U</dt>
111 * <dd>week number of the year as a decimal number (00-53)</dd>
112 * <dt>%V</dt>
113 * <dd>ISO 8601 week number as a decimal number (01-53)</dd>
114 * <dt>%w</dt>
115 * <dd>weekday as a decimal number (0-6)</dd>
116 * <dt>%W</dt>
117 * <dd>week number of the year as a decimal number (00-53)</dd>
118 * <dt>%x</dt>
119 * <dd>locale's date representation</dd>
120 * <dt>%X</dt>
121 * <dd>locale's time representation</dd>
122 * <dt>%y</dt>
123 * <dd>last 2 digits of the year as a decimal number (00-99)</dd>
124 * <dt>%Y</dt>
125 * <dd>year as a decimal number</dd>
126 * <dt>%z</dt>
127 * <dd>offset from UTC in the ISO 8601 format</dd>
128 * <dt>%Z</dt>
129 * <dd>locale's time zone name of abbreviation or empty</dd>
130 * </dl>
131 * 
132 * @author Guilhelm Savin
133 */
134public class ISODateIO {
135
136        private static final ISODateComponent[] KNOWN_COMPONENTS = {
137                        ISODateComponent.ABBREVIATED_WEEKDAY_NAME,
138                        ISODateComponent.FULL_WEEKDAY_NAME,
139                        ISODateComponent.ABBREVIATED_MONTH_NAME,
140                        ISODateComponent.FULL_MONTH_NAME,
141                        ISODateComponent.LOCALE_DATE_AND_TIME, ISODateComponent.CENTURY,
142                        ISODateComponent.DAY_OF_MONTH_2_DIGITS, ISODateComponent.DATE,
143                        ISODateComponent.DAY_OF_MONTH, ISODateComponent.DATE_ISO8601,
144                        ISODateComponent.WEEK_BASED_YEAR_2_DIGITS,
145                        ISODateComponent.WEEK_BASED_YEAR_4_DIGITS,
146                        ISODateComponent.ABBREVIATED_MONTH_NAME_ALIAS,
147                        ISODateComponent.HOUR_OF_DAY, ISODateComponent.HOUR,
148                        ISODateComponent.DAY_OF_YEAR, ISODateComponent.MILLISECOND,
149                        ISODateComponent.EPOCH, ISODateComponent.MONTH,
150                        ISODateComponent.MINUTE, ISODateComponent.NEW_LINE,
151                        ISODateComponent.AM_PM, ISODateComponent.LOCALE_CLOCK_TIME_12_HOUR,
152                        ISODateComponent.HOUR_AND_MINUTE, ISODateComponent.SECOND,
153                        ISODateComponent.TABULATION, ISODateComponent.TIME_ISO8601,
154                        ISODateComponent.DAY_OF_WEEK_1_7,
155                        ISODateComponent.WEEK_OF_YEAR_FROM_SUNDAY,
156                        ISODateComponent.WEEK_NUMBER_ISO8601,
157                        ISODateComponent.DAY_OF_WEEK_0_6,
158                        ISODateComponent.WEEK_OF_YEAR_FROM_MONDAY,
159                        ISODateComponent.LOCALE_DATE_REPRESENTATION,
160                        ISODateComponent.LOCALE_TIME_REPRESENTATION,
161                        ISODateComponent.YEAR_2_DIGITS, ISODateComponent.YEAR_4_DIGITS,
162                        ISODateComponent.UTC_OFFSET,
163                        ISODateComponent.LOCALE_TIME_ZONE_NAME, ISODateComponent.PERCENT };
164
165        /**
166         * List of components, build from a string format. Some of these components
167         * can just be text.
168         */
169        protected LinkedList<ISODateComponent> components;
170        /**
171         * The regular expression builds from the components.
172         */
173        protected Pattern pattern;
174
175        /**
176         * Create a scanner with default format "%K".
177         * 
178         * @throws ParseException
179         */
180        public ISODateIO() throws ParseException {
181                this("%K");
182        }
183
184        /**
185         * Create a new scanner with a given format.
186         * 
187         * @param format
188         *            format of the scanner.
189         * @throws ParseException
190         *             if bad directives found
191         */
192        public ISODateIO(String format) throws ParseException {
193                setFormat(format);
194        }
195
196        /**
197         * Get the current pattern used to parse timestamp.
198         * 
199         * @return a regular expression as a string
200         */
201        public Pattern getPattern() {
202                return pattern;
203        }
204
205        /**
206         * Build a list of component from a string.
207         * 
208         * @param format
209         *            format of the scanner
210         * @return a list of components found in the string format
211         * @throws ParseException
212         *             if invalid component found
213         */
214        protected LinkedList<ISODateComponent> findComponents(String format)
215                        throws ParseException {
216                LinkedList<ISODateComponent> components = new LinkedList<ISODateComponent>();
217                int offset = 0;
218
219                while (offset < format.length()) {
220                        if (format.charAt(offset) == '%') {
221                                boolean found = false;
222                                for (int i = 0; !found && i < KNOWN_COMPONENTS.length; i++) {
223                                        if (format.startsWith(KNOWN_COMPONENTS[i].getDirective(),
224                                                        offset)) {
225                                                found = true;
226                                                if (KNOWN_COMPONENTS[i].isAlias()) {
227                                                        LinkedList<ISODateComponent> sub = findComponents(KNOWN_COMPONENTS[i]
228                                                                        .getReplacement());
229                                                        components.addAll(sub);
230                                                } else
231                                                        components.addLast(KNOWN_COMPONENTS[i]);
232
233                                                offset += KNOWN_COMPONENTS[i].getDirective().length();
234                                        }
235                                }
236                                if (!found)
237                                        throw new ParseException("unknown identifier", offset);
238                        } else {
239                                int from = offset;
240                                while (offset < format.length() && format.charAt(offset) != '%')
241                                        offset++;
242                                components.addLast(new TextComponent(format.substring(from,
243                                                offset)));
244                        }
245                }
246
247                return components;
248        }
249
250        /**
251         * Build a regular expression from the components of the scanner.
252         */
253        protected void buildRegularExpression() {
254                String pattern = "";
255
256                for (int i = 0; i < components.size(); i++) {
257                        Object c = components.get(i);
258                        String regexValue;
259                        if (c instanceof ISODateComponent)
260                                regexValue = ((ISODateComponent) c).getReplacement();
261                        else
262                                regexValue = c.toString();
263
264                        pattern += "(" + regexValue + ")";
265                }
266
267                this.pattern = Pattern.compile(pattern);
268        }
269
270        /**
271         * Set the format of this scanner.
272         * 
273         * @param format
274         *            new format of the scanner
275         * @throws ParseException
276         *             if an error is found in the new format
277         */
278        public void setFormat(String format) throws ParseException {
279                components = findComponents(format);
280                buildRegularExpression();
281        }
282
283        /**
284         * Parse a string which should be in the scanner format. If not, null is
285         * returned.
286         * 
287         * @param time
288         *            timestamp in the scanner format
289         * @return a calendar modeling the time value or null if invalid format
290         */
291        public Calendar parse(String time) {
292                Calendar cal = Calendar.getInstance();
293                Matcher match = pattern.matcher(time);
294
295                if (match.matches()) {
296                        for (int i = 0; i < components.size(); i++)
297                                components.get(i).set(match.group(i + 1), cal);
298                } else
299                        return null;
300
301                return cal;
302        }
303
304        /**
305         * Convert a calendar into a string according to the format of this object.
306         * 
307         * @param calendar
308         *            the calendar to convert
309         * @return a string modeling the calendar.
310         */
311        public String toString(Calendar calendar) {
312                StringBuffer buffer = new StringBuffer();
313
314                for (int i = 0; i < components.size(); i++)
315                        buffer.append(components.get(i).get(calendar));
316
317                return buffer.toString();
318        }
319}