1 /*
   2  * Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 /*
  27  * This file is available under and governed by the GNU General Public
  28  * License version 2 only, as published by the Free Software Foundation.
  29  * However, the following notice accompanied the original version of this
  30  * file:
  31  *
  32  * Copyright (c) 2009-2012, Stephen Colebourne & Michael Nascimento Santos
  33  *
  34  * All rights reserved.
  35  *
  36  * Redistribution and use in source and binary forms, with or without
  37  * modification, are permitted provided that the following conditions are met:
  38  *
  39  *  * Redistributions of source code must retain the above copyright notice,
  40  *    this list of conditions and the following disclaimer.
  41  *
  42  *  * Redistributions in binary form must reproduce the above copyright notice,
  43  *    this list of conditions and the following disclaimer in the documentation
  44  *    and/or other materials provided with the distribution.
  45  *
  46  *  * Neither the name of JSR-310 nor the names of its contributors
  47  *    may be used to endorse or promote products derived from this software
  48  *    without specific prior written permission.
  49  *
  50  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  51  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  52  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  53  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
  54  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
  55  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  56  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
  57  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
  58  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  59  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  60  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  61  */
  62 package java.time;
  63 
  64 import java.time.format.DateTimeParseException;
  65 
  66 /**
  67  * A period parser that creates an instance of {@code Period} from a string.
  68  * This parses the ISO-8601 period format {@code PnYnMnDTnHnMn.nS}.
  69  * <p>
  70  * This class is mutable and intended for use by a single thread.
  71  *
  72  * @since 1.8
  73  */
  74 final class PeriodParser {
  75 
  76     /**
  77      * Used to validate the correct sequence of tokens.
  78      */
  79     private static final String TOKEN_SEQUENCE = "PYMDTHMS";
  80     /**
  81      * The standard string representing a zero period.
  82      */
  83     private static final String ZERO = "PT0S";
  84 
  85     /**
  86      * The number of years.
  87      */
  88     private int years;
  89     /**
  90      * The number of months.
  91      */
  92     private int months;
  93     /**
  94      * The number of days.
  95      */
  96     private int days;
  97     /**
  98      * The number of hours.
  99      */
 100     private int hours;
 101     /**
 102      * The number of minutes.
 103      */
 104     private int minutes;
 105     /**
 106      * The number of seconds.
 107      */
 108     private int seconds;
 109     /**
 110      * The number of nanoseconds.
 111      */
 112     private long nanos;
 113     /**
 114      * Whether the seconds were negative.
 115      */
 116     private boolean negativeSecs;
 117     /**
 118      * Parser position index.
 119      */
 120     private int index;
 121     /**
 122      * Original text.
 123      */
 124     private CharSequence text;
 125 
 126     /**
 127      * Constructor.
 128      *
 129      * @param text  the text to parse, not null
 130      */
 131     PeriodParser(CharSequence text) {
 132         this.text = text;
 133     }
 134 
 135     //-----------------------------------------------------------------------
 136     /**
 137      * Performs the parse.
 138      * <p>
 139      * This parses the text set in the constructor in the format PnYnMnDTnHnMn.nS.
 140      *
 141      * @return the created Period, not null
 142      * @throws DateTimeParseException if the text cannot be parsed to a Period
 143      */
 144     Period parse() {
 145         // force to upper case and coerce the comma to dot
 146 
 147         String s = text.toString().toUpperCase().replace(',', '.');
 148         // check for zero and skip parse
 149         if (ZERO.equals(s)) {
 150             return Period.ZERO;
 151         }
 152         if (s.length() < 3 || s.charAt(0) != 'P') {
 153             throw new DateTimeParseException("Period could not be parsed: " + text, text, 0);
 154         }
 155         validateCharactersAndOrdering(s, text);
 156 
 157         // strip off the leading P
 158         String[] datetime = s.substring(1).split("T");
 159         switch (datetime.length) {
 160             case 2:
 161                 parseDate(datetime[0], 1);
 162                 parseTime(datetime[1], datetime[0].length() + 2);
 163                 break;
 164             case 1:
 165                 parseDate(datetime[0], 1);
 166                 break;
 167         }
 168         return toPeriod();
 169     }
 170 
 171     private void parseDate(String s, int baseIndex) {
 172         index = 0;
 173         while (index < s.length()) {
 174             String value = parseNumber(s);
 175             if (index < s.length()) {
 176                 char c = s.charAt(index);
 177                 switch(c) {
 178                     case 'Y': years = parseInt(value, baseIndex) ; break;
 179                     case 'M': months = parseInt(value, baseIndex) ; break;
 180                     case 'D': days = parseInt(value, baseIndex) ; break;
 181                     default:
 182                         throw new DateTimeParseException("Period could not be parsed, unrecognized letter '" +
 183                                 c + ": " + text, text, baseIndex + index);
 184                 }
 185                 index++;
 186             }
 187         }
 188     }
 189 
 190     private void parseTime(String s, int baseIndex) {
 191         index = 0;
 192         s = prepareTime(s, baseIndex);
 193         while (index < s.length()) {
 194             String value = parseNumber(s);
 195             if (index < s.length()) {
 196                 char c = s.charAt(index);
 197                 switch(c) {
 198                     case 'H': hours = parseInt(value, baseIndex) ; break;
 199                     case 'M': minutes = parseInt(value, baseIndex) ; break;
 200                     case 'S': seconds = parseInt(value, baseIndex) ; break;
 201                     case 'N': nanos = parseNanos(value, baseIndex); break;
 202                     default:
 203                         throw new DateTimeParseException("Period could not be parsed, unrecognized letter '" +
 204                                 c + "': " + text, text, baseIndex + index);
 205                 }
 206                 index++;
 207             }
 208         }
 209     }
 210 
 211     private long parseNanos(String s, int baseIndex) {
 212         if (s.length() > 9) {
 213             throw new DateTimeParseException("Period could not be parsed, nanosecond range exceeded: " +
 214                     text, text, baseIndex + index - s.length());
 215         }
 216         // pad to the right to create 10**9, then trim
 217         return Long.parseLong((s + "000000000").substring(0, 9));
 218     }
 219 
 220     private String prepareTime(String s, int baseIndex) {
 221         if (s.contains(".")) {
 222             int i = s.indexOf(".") + 1;
 223 
 224             // verify that the first character after the dot is a digit
 225             if (Character.isDigit(s.charAt(i))) {
 226                 i++;
 227             } else {
 228                 throw new DateTimeParseException("Period could not be parsed, invalid decimal number: " +
 229                         text, text, baseIndex + index);
 230             }
 231 
 232             // verify that only digits follow the decimal point followed by an S
 233             while (i < s.length()) {
 234                 // || !Character.isDigit(s.charAt(i))
 235                 char c = s.charAt(i);
 236                 if (Character.isDigit(c) || c == 'S') {
 237                     i++;
 238                 } else {
 239                     throw new DateTimeParseException("Period could not be parsed, invalid decimal number: " +
 240                             text, text, baseIndex + index);
 241                 }
 242             }
 243             s = s.replace('S', 'N').replace('.', 'S');
 244             if (s.contains("-0S")) {
 245                 negativeSecs = true;
 246                 s = s.replace("-0S", "0S");
 247             }
 248         }
 249         return s;
 250     }
 251 
 252     private int parseInt(String s, int baseIndex) {
 253         try {
 254             int value = Integer.parseInt(s);
 255             if (s.charAt(0) == '-' && value == 0) {
 256                 throw new DateTimeParseException("Period could not be parsed, invalid number '" +
 257                         s + "': " + text, text, baseIndex + index - s.length());
 258             }
 259             return value;
 260         } catch (NumberFormatException ex) {
 261             throw new DateTimeParseException("Period could not be parsed, invalid number '" +
 262                     s + "': " + text, text, baseIndex + index - s.length());
 263         }
 264     }
 265 
 266     private String parseNumber(String s) {
 267         int start = index;
 268         while (index < s.length()) {
 269             char c = s.charAt(index);
 270             if ((c < '0' || c > '9') && c != '-') {
 271                 break;
 272             }
 273             index++;
 274         }
 275         return s.substring(start, index);
 276     }
 277 
 278     private void validateCharactersAndOrdering(String s, CharSequence text) {
 279         char[] chars = s.toCharArray();
 280         int tokenPos = 0;
 281         boolean lastLetter = false;
 282         for (int i = 0; i < chars.length; i++) {
 283             if (tokenPos >= TOKEN_SEQUENCE.length()) {
 284                 throw new DateTimeParseException("Period could not be parsed, characters after last 'S': " + text, text, i);
 285             }
 286             char c = chars[i];
 287             if ((c < '0' || c > '9') && c != '-' && c != '.') {
 288                 tokenPos = TOKEN_SEQUENCE.indexOf(c, tokenPos);
 289                 if (tokenPos < 0) {
 290                     throw new DateTimeParseException("Period could not be parsed, invalid character '" + c + "': " + text, text, i);
 291                 }
 292                 tokenPos++;
 293                 lastLetter = true;
 294             } else {
 295                 lastLetter = false;
 296             }
 297         }
 298         if (lastLetter == false) {
 299             throw new DateTimeParseException("Period could not be parsed, invalid last character: " + text, text, s.length() - 1);
 300         }
 301     }
 302 
 303     private Period toPeriod() {
 304         return Period.of(years, months, days, hours, minutes, seconds, negativeSecs || seconds < 0 ? -nanos : nanos);
 305     }
 306 
 307 }