• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

TAKETODAY / today-infrastructure / 18154768944

01 Oct 2025 07:26AM UTC coverage: 81.882% (-0.005%) from 81.887%
18154768944

push

github

web-flow
Merge pull request #290 from TAKETODAY/dev/jspecify

jspecify

59788 of 78013 branches covered (76.64%)

Branch coverage included in aggregate %.

141239 of 167496 relevant lines covered (84.32%)

3.6 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.52
today-context/src/main/java/infra/format/annotation/PeriodStyle.java
1
/*
2
 * Copyright 2017 - 2025 the original author or authors.
3
 *
4
 * This program is free software: you can redistribute it and/or modify
5
 * it under the terms of the GNU General Public License as published by
6
 * the Free Software Foundation, either version 3 of the License, or
7
 * (at your option) any later version.
8
 *
9
 * This program is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 * GNU General Public License for more details.
13
 *
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see [https://www.gnu.org/licenses/]
16
 */
17

18
package infra.format.annotation;
19

20
import org.jspecify.annotations.Nullable;
21

22
import java.time.Period;
23
import java.time.temporal.ChronoUnit;
24
import java.util.function.Function;
25
import java.util.regex.Matcher;
26
import java.util.regex.Pattern;
27

28
import infra.lang.Assert;
29

30
/**
31
 * A standard set of {@link Period} units.
32
 *
33
 * @author Eddú Meléndez
34
 * @author Edson Chávez
35
 * @author <a href="https://github.com/TAKETODAY">Harry Yang</a>
36
 * @see Period
37
 * @since 4.0
38
 */
39
public enum PeriodStyle {
2✔
40

41
  /**
42
   * Simple formatting, for example '1d'.
43
   */
44
  SIMPLE("^" + "(?:([-+]?[0-9]+)Y)?" + "(?:([-+]?[0-9]+)M)?" + "(?:([-+]?[0-9]+)W)?" + "(?:([-+]?[0-9]+)D)?" + "$", Pattern.CASE_INSENSITIVE) {
15✔
45
    @Override
46
    public Period parse(String value, @Nullable ChronoUnit unit) {
47
      try {
48
        if (NUMERIC.matcher(value).matches()) {
5✔
49
          return Unit.fromChronoUnit(unit).parse(value);
5✔
50
        }
51
        Matcher matcher = matcher(value);
4✔
52
        Assert.state(matcher.matches(), "Does not match simple period pattern");
4✔
53

54
        if (!hasAtLeastOneGroupValue(matcher)) {
4!
55
          throw new IllegalArgumentException("'" + value + "' is not a valid simple period");
×
56
        }
57

58
        int years = parseInt(matcher, 1);
5✔
59
        int months = parseInt(matcher, 2);
5✔
60
        int weeks = parseInt(matcher, 3);
5✔
61
        int days = parseInt(matcher, 4);
5✔
62
        return Period.of(years, months, Math.addExact(Math.multiplyExact(weeks, 7), days));
9✔
63
      }
64
      catch (Exception ex) {
1✔
65
        throw new IllegalArgumentException("'" + value + "' is not a valid simple period", ex);
7✔
66
      }
67
    }
68

69
    boolean hasAtLeastOneGroupValue(Matcher matcher) {
70
      for (int i = 0; i < matcher.groupCount(); i++) {
8!
71
        if (matcher.group(i + 1) != null) {
6✔
72
          return true;
2✔
73
        }
74
      }
75
      return false;
×
76
    }
77

78
    private int parseInt(Matcher matcher, int group) {
79
      String value = matcher.group(group);
4✔
80
      return (value != null) ? Integer.parseInt(value) : 0;
7✔
81
    }
82

83
    @Override
84
    protected boolean matches(String value) {
85
      return NUMERIC.matcher(value).matches() || matcher(value).matches();
14✔
86
    }
87

88
    @Override
89
    public String print(Period value, @Nullable ChronoUnit unit) {
90
      if (value.isZero()) {
3✔
91
        return Unit.fromChronoUnit(unit).print(value);
5✔
92
      }
93
      StringBuilder result = new StringBuilder();
4✔
94
      append(result, value, Unit.YEARS);
5✔
95
      append(result, value, Unit.MONTHS);
5✔
96
      append(result, value, Unit.DAYS);
5✔
97
      return result.toString();
3✔
98
    }
99

100
    private void append(StringBuilder result, Period value, Unit unit) {
101
      if (!unit.isZero(value)) {
4✔
102
        result.append(unit.print(value));
6✔
103
      }
104
    }
1✔
105

106
  },
107

108
  /**
109
   * ISO-8601 formatting.
110
   */
111
  ISO8601("^[+-]?P.*$", Pattern.CASE_INSENSITIVE) {
15✔
112
    @Override
113
    public Period parse(String value, @Nullable ChronoUnit unit) {
114
      try {
115
        return Period.parse(value);
3✔
116
      }
117
      catch (Exception ex) {
1✔
118
        throw new IllegalArgumentException("'" + value + "' is not a valid ISO-8601 period", ex);
7✔
119
      }
120
    }
121

122
    @Override
123
    public String print(Period value, @Nullable ChronoUnit unit) {
124
      return value.toString();
3✔
125
    }
126

127
  };
128

129
  private static final Pattern NUMERIC = Pattern.compile("^[-+]?[0-9]+$");
4✔
130

131
  private final Pattern pattern;
132

133
  PeriodStyle(String pattern, int flags) {
4✔
134
    this.pattern = Pattern.compile(pattern, flags);
5✔
135
  }
1✔
136

137
  protected boolean matches(String value) {
138
    return this.pattern.matcher(value).matches();
6✔
139
  }
140

141
  protected final Matcher matcher(String value) {
142
    return this.pattern.matcher(value);
5✔
143
  }
144

145
  /**
146
   * Parse the given value to a Period.
147
   *
148
   * @param value the value to parse
149
   * @return a period
150
   */
151
  public Period parse(String value) {
152
    return parse(value, null);
5✔
153
  }
154

155
  /**
156
   * Parse the given value to a period.
157
   *
158
   * @param value the value to parse
159
   * @param unit the period unit to use if the value doesn't specify one ({@code null}
160
   * will default to d)
161
   * @return a period
162
   */
163
  public abstract Period parse(String value, @Nullable ChronoUnit unit);
164

165
  /**
166
   * Print the specified period.
167
   *
168
   * @param value the value to print
169
   * @return the printed result
170
   */
171
  public String print(Period value) {
172
    return print(value, null);
5✔
173
  }
174

175
  /**
176
   * Print the specified period using the given unit.
177
   *
178
   * @param value the value to print
179
   * @param unit the value to use for printing
180
   * @return the printed result
181
   */
182
  public abstract String print(Period value, @Nullable ChronoUnit unit);
183

184
  /**
185
   * Detect the style then parse the value to return a period.
186
   *
187
   * @param value the value to parse
188
   * @return the parsed period
189
   * @throws IllegalArgumentException if the value is not a known style or cannot be
190
   * parsed
191
   */
192
  public static Period detectAndParse(String value) {
193
    return detectAndParse(value, null);
4✔
194
  }
195

196
  /**
197
   * Detect the style then parse the value to return a period.
198
   *
199
   * @param value the value to parse
200
   * @param unit the period unit to use if the value doesn't specify one ({@code null}
201
   * will default to ms)
202
   * @return the parsed period
203
   * @throws IllegalArgumentException if the value is not a known style or cannot be
204
   * parsed
205
   */
206
  public static Period detectAndParse(String value, @Nullable ChronoUnit unit) {
207
    return detect(value).parse(value, unit);
6✔
208
  }
209

210
  /**
211
   * Detect the style from the given source value.
212
   *
213
   * @param value the source value
214
   * @return the period style
215
   * @throws IllegalArgumentException if the value is not a known style
216
   */
217
  public static PeriodStyle detect(String value) {
218
    Assert.notNull(value, "Value is required");
3✔
219
    for (PeriodStyle candidate : values()) {
16✔
220
      if (candidate.matches(value)) {
4✔
221
        return candidate;
2✔
222
      }
223
    }
224
    throw new IllegalArgumentException("'" + value + "' is not a valid period");
6✔
225
  }
226

227
  private enum Unit {
3✔
228

229
    /**
230
     * Days, represented by suffix {@code d}.
231
     */
232
    DAYS(ChronoUnit.DAYS, "d", Period::getDays, Period::ofDays),
10✔
233

234
    /**
235
     * Weeks, represented by suffix {@code w}.
236
     */
237
    WEEKS(ChronoUnit.WEEKS, "w", null, Period::ofWeeks),
10✔
238

239
    /**
240
     * Months, represented by suffix {@code m}.
241
     */
242
    MONTHS(ChronoUnit.MONTHS, "m", Period::getMonths, Period::ofMonths),
10✔
243

244
    /**
245
     * Years, represented by suffix {@code y}.
246
     */
247
    YEARS(ChronoUnit.YEARS, "y", Period::getYears, Period::ofYears);
10✔
248

249
    private final ChronoUnit chronoUnit;
250

251
    private final String suffix;
252

253
    @Nullable
254
    private final Function<Period, Integer> intValue;
255

256
    private final Function<Integer, Period> factory;
257

258
    Unit(ChronoUnit chronoUnit, String suffix, @Nullable Function<Period, Integer> intValue,
259
            Function<Integer, Period> factory) {
4✔
260
      this.chronoUnit = chronoUnit;
3✔
261
      this.suffix = suffix;
3✔
262
      this.intValue = intValue;
3✔
263
      this.factory = factory;
3✔
264
    }
1✔
265

266
    private Period parse(String value) {
267
      return this.factory.apply(Integer.parseInt(value));
8✔
268
    }
269

270
    private String print(Period value) {
271
      return intValue(value) + this.suffix;
7✔
272
    }
273

274
    private boolean isZero(Period value) {
275
      return intValue(value) == 0;
8✔
276
    }
277

278
    private int intValue(Period value) {
279
      if (intValue == null) {
3!
280
        throw new IllegalArgumentException("intValue cannot be extracted from " + name());
×
281
      }
282
      return intValue.apply(value);
7✔
283
    }
284

285
    private static Unit fromChronoUnit(@Nullable ChronoUnit chronoUnit) {
286
      if (chronoUnit == null) {
2✔
287
        return Unit.DAYS;
2✔
288
      }
289
      for (Unit candidate : values()) {
16!
290
        if (candidate.chronoUnit == chronoUnit) {
4✔
291
          return candidate;
2✔
292
        }
293
      }
294
      throw new IllegalArgumentException("Unsupported unit " + chronoUnit);
×
295
    }
296

297
  }
298

299
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc