View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.lang3.time;
19  
20  import static org.junit.jupiter.api.Assertions.assertEquals;
21  import static org.junit.jupiter.api.Assertions.assertNotNull;
22  import static org.junit.jupiter.api.Assertions.assertNull;
23  import static org.junit.jupiter.api.Assertions.assertTrue;
24  
25  import java.nio.charset.StandardCharsets;
26  import java.nio.file.Files;
27  import java.nio.file.Path;
28  import java.nio.file.Paths;
29  import java.util.TimeZone;
30  
31  import org.apache.commons.lang3.AbstractLangTest;
32  import org.junit.jupiter.api.Test;
33  import org.junit.jupiter.params.ParameterizedTest;
34  import org.junit.jupiter.params.provider.ValueSource;
35  
36  /**
37   * Tests {@link FastTimeZone}.
38   */
39  class FastTimeZoneTest extends AbstractLangTest {
40  
41      private static final int HOURS_2 = 2 * 60 * 60 * 1000;
42      private static final int HOURS_23 = 23 * 60 * 60 * 1000;
43      private static final int MINUTES_5 = 5 * 60 * 1000;
44      private static final int MINUTES_59 = 59 * 60 * 1000;
45  
46      @Test
47      void testBareGmt() {
48          assertEquals(FastTimeZone.getGmtTimeZone(), FastTimeZone.getTimeZone(TimeZones.GMT_ID));
49      }
50  
51      @Test
52      void testEmptyStringReturnsNonNullDespiteJavadoc() {
53          // Javadoc claims null when pattern does not match. Empty string matches
54          // the over-permissive regex and returns the GMT zone instead.
55          final TimeZone tz = FastTimeZone.getGmtTimeZone("");
56          assertNotNull(tz);
57          assertEquals(0, tz.getRawOffset());
58      }
59  
60      @Test
61      void testGetGmtTimeZone() {
62          assertEquals(0, FastTimeZone.getGmtTimeZone().getRawOffset());
63      }
64  
65      @Test
66      void testGmtOnlyReturnsNonNull() {
67          // The literal "GMT" prefix on its own matches; both digit groups absent.
68          final TimeZone tz = FastTimeZone.getGmtTimeZone("GMT");
69          assertNotNull(tz);
70          assertEquals(0, tz.getRawOffset());
71      }
72  
73      @Test
74      void testGmtPrefix() {
75          assertEquals(HOURS_23, FastTimeZone.getGmtTimeZone("GMT+23:00").getRawOffset());
76          assertEquals(-HOURS_23, FastTimeZone.getGmtTimeZone("GMT-23:00").getRawOffset());
77      }
78  
79      @Test
80      void testHoursColonMinutes() {
81          assertEquals(HOURS_23, FastTimeZone.getGmtTimeZone("23:00").getRawOffset());
82          assertEquals(HOURS_2, FastTimeZone.getGmtTimeZone("2:00").getRawOffset());
83          assertEquals(MINUTES_59, FastTimeZone.getGmtTimeZone("00:59").getRawOffset());
84          assertEquals(MINUTES_5, FastTimeZone.getGmtTimeZone("00:5").getRawOffset());
85          assertEquals(HOURS_23 + MINUTES_59, FastTimeZone.getGmtTimeZone("23:59").getRawOffset());
86          assertEquals(HOURS_2 + MINUTES_5, FastTimeZone.getGmtTimeZone("2:5").getRawOffset());
87      }
88  
89      @Test
90      void testHoursMinutes() {
91          assertEquals(HOURS_23, FastTimeZone.getGmtTimeZone("2300").getRawOffset());
92          assertEquals(HOURS_2, FastTimeZone.getGmtTimeZone("0200").getRawOffset());
93          assertEquals(MINUTES_59, FastTimeZone.getGmtTimeZone("0059").getRawOffset());
94          assertEquals(MINUTES_5, FastTimeZone.getGmtTimeZone("0005").getRawOffset());
95          assertEquals(HOURS_23 + MINUTES_59, FastTimeZone.getGmtTimeZone("2359").getRawOffset());
96          assertEquals(HOURS_2 + MINUTES_5, FastTimeZone.getGmtTimeZone("0205").getRawOffset());
97      }
98  
99      @Test
100     void testInvalidStringReturnsNull() {
101         // Negative control: a string that genuinely cannot match the regex returns null.
102         assertNull(FastTimeZone.getGmtTimeZone("XYZ"), "non-matching input returns null");
103     }
104 
105     /**
106      * Patched-source check. After the doc-only patch lands, the Javadoc on {@code getGmtTimeZone(String)} no longer promises "null if pattern does not match"
107      * for the empty / sign-only / GMT-only inputs. We string-search the source file for the corrected wording so reverting the Javadoc (mutation control) flips
108      * this assertion to FAIL.
109      */
110     @Test
111     void testJavadocReflectsLenientBehavior() throws Exception {
112         final Path src = Paths.get("src/main/java/org/apache/commons/lang3/time/FastTimeZone.java");
113         final String body = new String(Files.readAllBytes(src), StandardCharsets.UTF_8);
114         assertTrue(body.contains("defaulting to GMT for an unrecognized but parseable input"));
115     }
116 
117     @Test
118     void testOlson() {
119         assertEquals(TimeZones.getTimeZone("America/New_York"), FastTimeZone.getTimeZone("America/New_York"));
120     }
121 
122     /**
123      * A pattern that matches the regex but whose hours or minutes are out of range is not a valid GMT id. Before the fix these threw IllegalArgumentException
124      * from the GmtTimeZone constructor.
125      */
126     @ParameterizedTest
127     @ValueSource(strings = { "GMT+24", "+24", "-24", "+99", "+12:60", "00:99", "99:99", "0099", "9999" })
128     void testOutOfRangeOffsetReturnsNull(final String pattern) {
129         assertNull(FastTimeZone.getGmtTimeZone(pattern));
130     }
131 
132     @Test
133     void testOutOfRangeOffsetViaGetTimeZoneFallsBack() {
134         // getTimeZone must not propagate the constructor exception; a null GMT result falls back to the JDK lookup.
135         final TimeZone tz = FastTimeZone.getTimeZone("GMT+24");
136         assertNotNull(tz);
137         assertEquals(0, tz.getRawOffset());
138     }
139 
140     @Test
141     void testPlusOnlyReturnsNonNull() {
142         // Sign-only input still matches the regex; hours and minutes default to 0.
143         final TimeZone tz = FastTimeZone.getGmtTimeZone("+");
144         assertNotNull(tz);
145         assertEquals(0, tz.getRawOffset());
146     }
147 
148     @Test
149     void testSign() {
150         assertEquals(HOURS_23, FastTimeZone.getGmtTimeZone("+23:00").getRawOffset());
151         assertEquals(HOURS_2, FastTimeZone.getGmtTimeZone("+2:00").getRawOffset());
152         assertEquals(-HOURS_23, FastTimeZone.getGmtTimeZone("-23:00").getRawOffset());
153         assertEquals(-HOURS_2, FastTimeZone.getGmtTimeZone("-2:00").getRawOffset());
154     }
155 
156     @Test
157     void testUTC() {
158         assertEquals(FastTimeZone.getGmtTimeZone(), FastTimeZone.getTimeZone("UTC"));
159     }
160 
161     @Test
162     void testZ() {
163         assertEquals(FastTimeZone.getGmtTimeZone(), FastTimeZone.getTimeZone("Z"));
164     }
165 
166     @Test
167     void testZeroOffsetsReturnSingleton() {
168         assertEquals(FastTimeZone.getGmtTimeZone(), FastTimeZone.getTimeZone("+0"));
169         assertEquals(FastTimeZone.getGmtTimeZone(), FastTimeZone.getTimeZone("-0"));
170     }
171 }