jdk/src/share/classes/java/time/PeriodParser.java
changeset 15658 55b829ca2334
parent 15657 c588664d547e
child 15659 e575dab44ff5
equal deleted inserted replaced
15657:c588664d547e 15658:55b829ca2334
     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 }