From 1d114783609b4c6673494153a984b14434ed294a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=CC=88rg=20Prante?= Date: Wed, 2 Nov 2016 16:40:54 +0100 Subject: [PATCH] fixes for Sonarqube --- CREDITS.txt | 18 + .../java/org/xbib/time/chronic/Chronic.java | 17 +- src/main/java/org/xbib/time/chronic/Span.java | 13 + .../java/org/xbib/time/chronic/Token.java | 2 +- .../xbib/time/chronic/handlers/Handler.java | 18 +- .../xbib/time/chronic/handlers/IHandler.java | 1 + .../time/chronic/handlers/ORGRHandler.java | 1 + .../time/chronic/handlers/ORSRHandler.java | 1 + .../xbib/time/chronic/handlers/RHandler.java | 1 + .../handlers/RdnRmnSdTTzSyHandler.java | 6 + .../time/chronic/handlers/RmnOdHandler.java | 2 + .../time/chronic/handlers/RmnSdHandler.java | 2 + .../time/chronic/handlers/RmnSdSyHandler.java | 6 + .../time/chronic/handlers/RmnSyHandler.java | 6 + .../time/chronic/handlers/SmSdHandler.java | 2 + .../time/chronic/handlers/SmSdSyHandler.java | 6 + .../time/chronic/handlers/SmSyHandler.java | 6 + .../time/chronic/numerizer/Numerizer.java | 6 +- .../xbib/time/chronic/repeaters/Repeater.java | 13 + .../time/chronic/repeaters/RepeaterDay.java | 12 + .../chronic/repeaters/RepeaterDayName.java | 21 +- .../chronic/repeaters/RepeaterDayPortion.java | 13 +- .../chronic/repeaters/RepeaterFortnight.java | 18 +- .../time/chronic/repeaters/RepeaterHour.java | 12 + .../chronic/repeaters/RepeaterMinute.java | 12 + .../time/chronic/repeaters/RepeaterMonth.java | 12 + .../chronic/repeaters/RepeaterMonthName.java | 16 +- .../chronic/repeaters/RepeaterSecond.java | 14 +- .../time/chronic/repeaters/RepeaterTime.java | 35 +- .../time/chronic/repeaters/RepeaterUnit.java | 16 +- .../time/chronic/repeaters/RepeaterWeek.java | 16 +- .../chronic/repeaters/RepeaterWeekend.java | 47 +- .../time/chronic/repeaters/RepeaterYear.java | 12 + .../java/org/xbib/time/format/Composite.java | 109 ++ .../org/xbib/time/format/CompositeAffix.java | 82 ++ .../org/xbib/time/format/FieldFormatter.java | 303 +++++ .../org/xbib/time/format/FormatUtils.java | 101 +- .../org/xbib/time/format/IgnorableAffix.java | 69 ++ .../java/org/xbib/time/format/Literal.java | 42 + .../xbib/time/format/PeriodFieldAffix.java | 46 + .../time/format/PeriodFormatterBuilder.java | 1060 +---------------- .../org/xbib/time/format/PluralAffix.java | 82 ++ .../java/org/xbib/time/format/RegExAffix.java | 97 ++ .../java/org/xbib/time/format/Separator.java | 180 +++ .../org/xbib/time/format/SimpleAffix.java | 75 ++ .../java/org/xbib/time/pretty/PrettyTime.java | 15 +- .../xbib/time/pretty/i18n/Resources_fi.java | 4 +- .../org/xbib/time/chronic/ParserTest.java | 2 +- 48 files changed, 1475 insertions(+), 1175 deletions(-) create mode 100644 CREDITS.txt create mode 100644 src/main/java/org/xbib/time/format/Composite.java create mode 100644 src/main/java/org/xbib/time/format/CompositeAffix.java create mode 100644 src/main/java/org/xbib/time/format/FieldFormatter.java create mode 100644 src/main/java/org/xbib/time/format/IgnorableAffix.java create mode 100644 src/main/java/org/xbib/time/format/Literal.java create mode 100644 src/main/java/org/xbib/time/format/PeriodFieldAffix.java create mode 100644 src/main/java/org/xbib/time/format/PluralAffix.java create mode 100644 src/main/java/org/xbib/time/format/RegExAffix.java create mode 100644 src/main/java/org/xbib/time/format/Separator.java create mode 100644 src/main/java/org/xbib/time/format/SimpleAffix.java diff --git a/CREDITS.txt b/CREDITS.txt new file mode 100644 index 0000000..c42ab7d --- /dev/null +++ b/CREDITS.txt @@ -0,0 +1,18 @@ + +org.xbib.time is based upon the following software: + +- Chronic https://github.com/samtingleff/jchronic (MIT License) + +- org.joda.time.format https://github.com/JodaOrg/joda-time (Apache 2.0) + +- prettytime https://github.com/ocpsoft/prettytime (Apache 2.0) + +with improvements by Jörg Prante including + +- converted to Java 8 java.time API + +- fixing bugs reported by Sonarqube + +- refactoring classes + +- rewritten code to simplify and ease development diff --git a/src/main/java/org/xbib/time/chronic/Chronic.java b/src/main/java/org/xbib/time/chronic/Chronic.java index 4b62eae..5711388 100644 --- a/src/main/java/org/xbib/time/chronic/Chronic.java +++ b/src/main/java/org/xbib/time/chronic/Chronic.java @@ -3,17 +3,26 @@ package org.xbib.time.chronic; import org.xbib.time.chronic.handlers.Handler; import org.xbib.time.chronic.numerizer.Numerizer; import org.xbib.time.chronic.repeaters.Repeater; -import org.xbib.time.chronic.tags.*; +import org.xbib.time.chronic.tags.Grabber; +import org.xbib.time.chronic.tags.Ordinal; +import org.xbib.time.chronic.tags.Pointer; +import org.xbib.time.chronic.tags.Scalar; +import org.xbib.time.chronic.tags.Separator; +import org.xbib.time.chronic.tags.TimeZone; import java.text.ParseException; import java.util.LinkedList; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; /** * */ public class Chronic { + private static final Logger logger = Logger.getLogger(Chronic.class.getName()); + private Chronic() { } @@ -74,7 +83,8 @@ public class Chronic { try { tokens = (List) optionScannerClass.getMethod("scan", List.class, Options.class) .invoke(null, tokens, options); - } catch (Throwable e) { + } catch (Exception e) { + logger.log(Level.FINE, e.getMessage(), e); throw new ParseException("failed to scan tokens", 0); } } @@ -89,7 +99,8 @@ public class Chronic { try { tokens = (List) scannerClass.getMethod("scan", List.class, Options.class) .invoke(null, tokens, options); - } catch (Throwable e) { + } catch (Exception e) { + logger.log(Level.FINE, e.getMessage(), e); throw new ParseException("failed to scan tokens", 0); } } diff --git a/src/main/java/org/xbib/time/chronic/Span.java b/src/main/java/org/xbib/time/chronic/Span.java index 940eb7f..50ca868 100644 --- a/src/main/java/org/xbib/time/chronic/Span.java +++ b/src/main/java/org/xbib/time/chronic/Span.java @@ -46,6 +46,19 @@ public class Span extends Range { return zoneId; } + @Override + public int hashCode() { + return super.hashCode() ^ zoneId.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof Span && + ((Span) obj).getBegin().equals(getBegin()) && + ((Span) obj).getEnd().equals(getEnd()) && + ((Span) obj).zoneId.equals(zoneId); + } + @Override public String toString() { return "(" + DateTimeFormatter.ISO_INSTANT.format(getBeginCalendar()) diff --git a/src/main/java/org/xbib/time/chronic/Token.java b/src/main/java/org/xbib/time/chronic/Token.java index 13d07f7..c202cb8 100644 --- a/src/main/java/org/xbib/time/chronic/Token.java +++ b/src/main/java/org/xbib/time/chronic/Token.java @@ -62,7 +62,7 @@ public class Token { public > T getTag(Class tagClass) { List matches = getTags(tagClass); T matchingTag = null; - if (matches.size() > 0) { + if (!matches.isEmpty()) { matchingTag = matches.get(0); } return matchingTag; diff --git a/src/main/java/org/xbib/time/chronic/handlers/Handler.java b/src/main/java/org/xbib/time/chronic/handlers/Handler.java index aa0214f..9876fc1 100644 --- a/src/main/java/org/xbib/time/chronic/handlers/Handler.java +++ b/src/main/java/org/xbib/time/chronic/handlers/Handler.java @@ -30,11 +30,7 @@ import org.xbib.time.chronic.tags.TimeZone; import java.text.ParseException; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; +import java.util.*; /** * @@ -57,7 +53,7 @@ public class Handler { public static synchronized Map> definitions() { if (definitions == null) { - Map> definitions = new HashMap<>(); + Map> definitions = new EnumMap<>(HandlerType.class); List timeHandlers = new LinkedList<>(); timeHandlers.add(new Handler(null, new TagPattern(RepeaterTime.class), @@ -227,7 +223,7 @@ public class Handler { if (grabberType == Grabber.Relative.LAST) { outerSpan = head.nextSpan(PointerType.PAST); } else if (grabberType == Grabber.Relative.THIS) { - if (repeaters.size() > 0) { + if (!repeaters.isEmpty()) { outerSpan = head.thisSpan(PointerType.NONE); } else { outerSpan = head.thisSpan(options.getContext()); @@ -273,11 +269,11 @@ public class Handler { } @SuppressWarnings("unchecked") - public static List dealiasAndDisambiguateTimes(List tokens, Options options) { + public static List dealiasAndDisambiguateTimes(List tokenList, Options options) { // handle aliases of am/pm // 5:00 in the morning => 5:00 am // 7:00 in the evening => 7:00 pm - + List tokens = tokenList; int dayPortionIndex = -1; int tokenSize = tokens.size(); for (int i = 0; dayPortionIndex == -1 && i < tokenSize; i++) { @@ -347,8 +343,8 @@ public class Handler { for (HandlerPattern pattern : patterns) { boolean optional = pattern.isOptional(); if (pattern instanceof TagPattern) { - boolean match = (tokenIndex < tokens.size() && - tokens.get(tokenIndex).getTags(((TagPattern) pattern).getTagClass()).size() > 0); + boolean match = tokenIndex < tokens.size() && + tokens.get(tokenIndex).getTags(((TagPattern) pattern).getTagClass()).size() > 0; if (!match && !optional) { return false; } diff --git a/src/main/java/org/xbib/time/chronic/handlers/IHandler.java b/src/main/java/org/xbib/time/chronic/handlers/IHandler.java index 771f467..bdbc990 100644 --- a/src/main/java/org/xbib/time/chronic/handlers/IHandler.java +++ b/src/main/java/org/xbib/time/chronic/handlers/IHandler.java @@ -9,6 +9,7 @@ import java.util.List; /** * */ +@FunctionalInterface public interface IHandler { Span handle(List tokens, Options options); } diff --git a/src/main/java/org/xbib/time/chronic/handlers/ORGRHandler.java b/src/main/java/org/xbib/time/chronic/handlers/ORGRHandler.java index f103ff4..52a8e86 100644 --- a/src/main/java/org/xbib/time/chronic/handlers/ORGRHandler.java +++ b/src/main/java/org/xbib/time/chronic/handlers/ORGRHandler.java @@ -11,6 +11,7 @@ import java.util.List; */ public class ORGRHandler extends ORRHandler { + @Override public Span handle(List tokens, Options options) { Span outerSpan = Handler.getAnchor(tokens.subList(2, 4), options); return handle(tokens.subList(0, 2), outerSpan, options); diff --git a/src/main/java/org/xbib/time/chronic/handlers/ORSRHandler.java b/src/main/java/org/xbib/time/chronic/handlers/ORSRHandler.java index 0cba6e3..cf48eb6 100644 --- a/src/main/java/org/xbib/time/chronic/handlers/ORSRHandler.java +++ b/src/main/java/org/xbib/time/chronic/handlers/ORSRHandler.java @@ -11,6 +11,7 @@ import java.util.List; */ public class ORSRHandler extends ORRHandler { + @Override public Span handle(List tokens, Options options) { Span outerSpan = Handler.getAnchor(tokens.subList(3, 4), options); return handle(tokens.subList(0, 2), outerSpan, options); diff --git a/src/main/java/org/xbib/time/chronic/handlers/RHandler.java b/src/main/java/org/xbib/time/chronic/handlers/RHandler.java index 8c756bd..25f8cbc 100644 --- a/src/main/java/org/xbib/time/chronic/handlers/RHandler.java +++ b/src/main/java/org/xbib/time/chronic/handlers/RHandler.java @@ -11,6 +11,7 @@ import java.util.List; */ public class RHandler implements IHandler { + @Override public Span handle(List tokens, Options options) { List ddTokens = Handler.dealiasAndDisambiguateTimes(tokens, options); return Handler.getAnchor(ddTokens, options); diff --git a/src/main/java/org/xbib/time/chronic/handlers/RdnRmnSdTTzSyHandler.java b/src/main/java/org/xbib/time/chronic/handlers/RdnRmnSdTTzSyHandler.java index f78f282..e3e5f13 100644 --- a/src/main/java/org/xbib/time/chronic/handlers/RdnRmnSdTTzSyHandler.java +++ b/src/main/java/org/xbib/time/chronic/handlers/RdnRmnSdTTzSyHandler.java @@ -9,12 +9,17 @@ import org.xbib.time.chronic.tags.ScalarYear; import java.time.ZonedDateTime; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; /** * */ public class RdnRmnSdTTzSyHandler implements IHandler { + private static final Logger logger = Logger.getLogger(RdnRmnSdTTzSyHandler.class.getName()); + + @Override public Span handle(List tokens, Options options) { int month = tokens.get(1).getTag(RepeaterMonthName.class).getType().ordinal(); int day = tokens.get(2).getTag(ScalarDay.class).getType(); @@ -25,6 +30,7 @@ public class RdnRmnSdTTzSyHandler implements IHandler { ZonedDateTime dayStart = ZonedDateTime.of(year, month, day, 0, 0, 0, 0, options.getZoneId()); span = Handler.dayOrTime(dayStart, timeTokens, options); } catch (IllegalArgumentException e) { + logger.log(Level.FINE, e.getMessage(), e); span = null; } return span; diff --git a/src/main/java/org/xbib/time/chronic/handlers/RmnOdHandler.java b/src/main/java/org/xbib/time/chronic/handlers/RmnOdHandler.java index 2640d70..046b9c2 100644 --- a/src/main/java/org/xbib/time/chronic/handlers/RmnOdHandler.java +++ b/src/main/java/org/xbib/time/chronic/handlers/RmnOdHandler.java @@ -12,6 +12,8 @@ import java.util.List; * */ public class RmnOdHandler extends MDHandler { + + @Override public Span handle(List tokens, Options options) { return handle(tokens.get(0).getTag(RepeaterMonthName.class), tokens.get(1).getTag(OrdinalDay.class), tokens.subList(2, tokens.size()), options); diff --git a/src/main/java/org/xbib/time/chronic/handlers/RmnSdHandler.java b/src/main/java/org/xbib/time/chronic/handlers/RmnSdHandler.java index b1ce4b6..ca59beb 100644 --- a/src/main/java/org/xbib/time/chronic/handlers/RmnSdHandler.java +++ b/src/main/java/org/xbib/time/chronic/handlers/RmnSdHandler.java @@ -12,6 +12,8 @@ import java.util.List; * */ public class RmnSdHandler extends MDHandler { + + @Override public Span handle(List tokens, Options options) { return handle(tokens.get(0).getTag(RepeaterMonthName.class), tokens.get(1).getTag(ScalarDay.class), tokens.subList(2, tokens.size()), options); diff --git a/src/main/java/org/xbib/time/chronic/handlers/RmnSdSyHandler.java b/src/main/java/org/xbib/time/chronic/handlers/RmnSdSyHandler.java index 2e4a36f..3a784d7 100644 --- a/src/main/java/org/xbib/time/chronic/handlers/RmnSdSyHandler.java +++ b/src/main/java/org/xbib/time/chronic/handlers/RmnSdSyHandler.java @@ -9,12 +9,17 @@ import org.xbib.time.chronic.tags.ScalarYear; import java.time.ZonedDateTime; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; /** * */ public class RmnSdSyHandler implements IHandler { + private static final Logger logger = Logger.getLogger(RmnSdSyHandler.class.getName()); + + @Override public Span handle(List tokens, Options options) { int month = tokens.get(0).getTag(RepeaterMonthName.class).getType().ordinal(); int day = tokens.get(1).getTag(ScalarDay.class).getType(); @@ -25,6 +30,7 @@ public class RmnSdSyHandler implements IHandler { ZonedDateTime dayStart = ZonedDateTime.of(year, month, day, 0, 0, 0, 0, options.getZoneId()); span = Handler.dayOrTime(dayStart, timeTokens, options); } catch (IllegalArgumentException e) { + logger.log(Level.FINE, e.getMessage(), e); span = null; } return span; diff --git a/src/main/java/org/xbib/time/chronic/handlers/RmnSyHandler.java b/src/main/java/org/xbib/time/chronic/handlers/RmnSyHandler.java index ba28063..9e9e94c 100644 --- a/src/main/java/org/xbib/time/chronic/handlers/RmnSyHandler.java +++ b/src/main/java/org/xbib/time/chronic/handlers/RmnSyHandler.java @@ -9,12 +9,17 @@ import org.xbib.time.chronic.tags.ScalarYear; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; /** * */ public class RmnSyHandler implements IHandler { + private static final Logger logger = Logger.getLogger(RmnSyHandler.class.getName()); + + @Override public Span handle(List tokens, Options options) { int month = tokens.get(0).getTag(RepeaterMonthName.class).getType().ordinal(); int year = tokens.get(1).getTag(ScalarYear.class).getType(); @@ -24,6 +29,7 @@ public class RmnSyHandler implements IHandler { ZonedDateTime end = start.plus(1, ChronoUnit.MONTHS); span = new Span(start, end); } catch (IllegalArgumentException e) { + logger.log(Level.FINE, e.getMessage(), e); span = null; } return span; diff --git a/src/main/java/org/xbib/time/chronic/handlers/SmSdHandler.java b/src/main/java/org/xbib/time/chronic/handlers/SmSdHandler.java index 7b63f70..399a809 100644 --- a/src/main/java/org/xbib/time/chronic/handlers/SmSdHandler.java +++ b/src/main/java/org/xbib/time/chronic/handlers/SmSdHandler.java @@ -14,6 +14,8 @@ import java.util.List; * */ public class SmSdHandler implements IHandler { + + @Override public Span handle(List tokens, Options options) { int month = tokens.get(0).getTag(ScalarMonth.class).getType(); int day = tokens.get(1).getTag(ScalarDay.class).getType(); diff --git a/src/main/java/org/xbib/time/chronic/handlers/SmSdSyHandler.java b/src/main/java/org/xbib/time/chronic/handlers/SmSdSyHandler.java index de6c6ed..e979b9e 100644 --- a/src/main/java/org/xbib/time/chronic/handlers/SmSdSyHandler.java +++ b/src/main/java/org/xbib/time/chronic/handlers/SmSdSyHandler.java @@ -9,12 +9,17 @@ import org.xbib.time.chronic.tags.ScalarYear; import java.time.ZonedDateTime; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; /** * */ public class SmSdSyHandler implements IHandler { + private static final Logger logger = Logger.getLogger(SmSdSyHandler.class.getName()); + + @Override public Span handle(List tokens, Options options) { int month = tokens.get(0).getTag(ScalarMonth.class).getType(); int day = tokens.get(1).getTag(ScalarDay.class).getType(); @@ -25,6 +30,7 @@ public class SmSdSyHandler implements IHandler { ZonedDateTime dayStart = ZonedDateTime.of(year, month, day, 0, 0, 0, 0, options.getZoneId()); span = Handler.dayOrTime(dayStart, timeTokens, options); } catch (IllegalArgumentException e) { + logger.log(Level.FINE, e.getMessage(), e); span = null; } return span; diff --git a/src/main/java/org/xbib/time/chronic/handlers/SmSyHandler.java b/src/main/java/org/xbib/time/chronic/handlers/SmSyHandler.java index 4f49309..29a0574 100644 --- a/src/main/java/org/xbib/time/chronic/handlers/SmSyHandler.java +++ b/src/main/java/org/xbib/time/chronic/handlers/SmSyHandler.java @@ -9,12 +9,17 @@ import org.xbib.time.chronic.tags.ScalarYear; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; /** * */ public class SmSyHandler implements IHandler { + private static final Logger logger = Logger.getLogger(SmSyHandler.class.getName()); + + @Override public Span handle(List tokens, Options options) { int month = tokens.get(0).getTag(ScalarMonth.class).getType(); int year = tokens.get(1).getTag(ScalarYear.class).getType(); @@ -24,6 +29,7 @@ public class SmSyHandler implements IHandler { ZonedDateTime end = start.plus(1, ChronoUnit.MONTHS); span = new Span(start, end); } catch (IllegalArgumentException e) { + logger.log(Level.FINE, e.getMessage(), e); span = null; } return span; diff --git a/src/main/java/org/xbib/time/chronic/numerizer/Numerizer.java b/src/main/java/org/xbib/time/chronic/numerizer/Numerizer.java index cde8e13..6380623 100644 --- a/src/main/java/org/xbib/time/chronic/numerizer/Numerizer.java +++ b/src/main/java/org/xbib/time/chronic/numerizer/Numerizer.java @@ -125,10 +125,10 @@ public class Numerizer { } private static String andition(String str) { - StringBuffer anditionStr = new StringBuffer(str); + StringBuilder anditionStr = new StringBuilder(str); Matcher matcher = Numerizer.ANDITION_PATTERN.matcher(anditionStr); while (matcher.find()) { - if (matcher.group(2).equalsIgnoreCase(" and ") || matcher.group(1).length() > matcher.group(3).length()) { + if (" and ".equalsIgnoreCase(matcher.group(2)) || matcher.group(1).length() > matcher.group(3).length()) { anditionStr.replace(matcher.start(), matcher.end(), String.valueOf(Integer.parseInt(matcher.group(1).trim()) + Integer.parseInt(matcher.group(3).trim()))); @@ -141,7 +141,7 @@ public class Numerizer { /** * */ - static class DirectNum { + private static class DirectNum { private Pattern name; private String number; diff --git a/src/main/java/org/xbib/time/chronic/repeaters/Repeater.java b/src/main/java/org/xbib/time/chronic/repeaters/Repeater.java index ca17993..40bb080 100644 --- a/src/main/java/org/xbib/time/chronic/repeaters/Repeater.java +++ b/src/main/java/org/xbib/time/chronic/repeaters/Repeater.java @@ -49,6 +49,7 @@ public abstract class Repeater extends Tag implements Comparable other) { return Integer.compare(getWidth(), other.getWidth()); } @@ -84,6 +85,18 @@ public abstract class Repeater extends Tag implements Comparable { - public static final int DAY_SECONDS = 86400; // (24 * 60 * 60); + private static final int DAY_SECONDS = 86400; private static final Pattern MON_PATTERN = Pattern.compile("^m[ou]n(day)?$"); private static final Pattern TUE_PATTERN = Pattern.compile("^t(ue|eu|oo|u|)s(day)?$"); private static final Pattern TUE_PATTERN_1 = Pattern.compile("^tue$"); @@ -69,17 +69,14 @@ public class RepeaterDayName extends Repeater { currentDayStart = currentDayStart.plus(direction, ChronoUnit.DAYS); } } else { - currentDayStart = currentDayStart.plus(direction * 7, ChronoUnit.DAYS); + currentDayStart = currentDayStart.plus(direction * 7L, ChronoUnit.DAYS); } return new Span(currentDayStart, ChronoUnit.DAYS, 1); } @Override protected Span internalThisSpan(PointerType pointer) { - if (pointer == PointerType.NONE) { - pointer = PointerType.FUTURE; - } - return super.nextSpan(pointer); + return super.nextSpan(pointer == PointerType.NONE ? PointerType.FUTURE : pointer); } @Override @@ -92,6 +89,18 @@ public class RepeaterDayName extends Repeater { return RepeaterDayName.DAY_SECONDS; } + @Override + public int hashCode() { + return super.hashCode() ^ getWidth(); + } + + @Override + public boolean equals(Object other) { + return other instanceof RepeaterDayName && + ((Repeater) other).getType().equals(getType()) && + ((Repeater) other).getNow().equals(getNow()); + } + @Override public String toString() { return super.toString() + "-dayname-" + getType(); diff --git a/src/main/java/org/xbib/time/chronic/repeaters/RepeaterDayPortion.java b/src/main/java/org/xbib/time/chronic/repeaters/RepeaterDayPortion.java index 5aa879d..6054dba 100644 --- a/src/main/java/org/xbib/time/chronic/repeaters/RepeaterDayPortion.java +++ b/src/main/java/org/xbib/time/chronic/repeaters/RepeaterDayPortion.java @@ -58,7 +58,6 @@ public abstract class RepeaterDayPortion extends Repeater { @Override protected Span internalNextSpan(PointerType pointer) { ZonedDateTime rangeStart; - ZonedDateTime rangeEnd; if (currentSpan == null) { long nowSeconds = getNow().toInstant().getEpochSecond() - ymd(getNow()).toInstant().getEpochSecond(); if (nowSeconds < range.getBegin()) { @@ -133,6 +132,18 @@ public abstract class RepeaterDayPortion extends Repeater { protected abstract Range createRange(T type); + @Override + public int hashCode() { + return super.hashCode() ^ getWidth(); + } + + @Override + public boolean equals(Object other) { + return other instanceof RepeaterDayPortion && + ((Repeater) other).getType().equals(getType()) && + ((Repeater) other).getNow().equals(getNow()); + } + @Override public String toString() { return super.toString() + "-dayportion-" + getType(); diff --git a/src/main/java/org/xbib/time/chronic/repeaters/RepeaterFortnight.java b/src/main/java/org/xbib/time/chronic/repeaters/RepeaterFortnight.java index b501dd3..982905f 100644 --- a/src/main/java/org/xbib/time/chronic/repeaters/RepeaterFortnight.java +++ b/src/main/java/org/xbib/time/chronic/repeaters/RepeaterFortnight.java @@ -35,7 +35,7 @@ public class RepeaterFortnight extends RepeaterUnit { Span lastSundaySpan = sundayRepeater.nextSpan(PointerType.PAST); currentFortnightStart = lastSundaySpan.getBeginCalendar(); } else { - throw new IllegalArgumentException("Unable to handle pointer " + pointer + "."); + throw new IllegalArgumentException("Unable to handle pointer " + pointer); } } else { long direction = (pointer == PointerType.FUTURE) ? 1L : -1L; @@ -47,11 +47,11 @@ public class RepeaterFortnight extends RepeaterUnit { } @Override - protected Span internalThisSpan(PointerType pointer) { + protected Span internalThisSpan(PointerType pointerType) { + PointerType pointer = pointerType; if (pointer == null) { pointer = PointerType.FUTURE; } - Span span; if (pointer == PointerType.FUTURE) { ZonedDateTime thisFortnightStart = ymdh(getNow()).plus(RepeaterHour.HOUR_SECONDS, ChronoUnit.SECONDS); @@ -86,6 +86,18 @@ public class RepeaterFortnight extends RepeaterUnit { return RepeaterFortnight.FORTNIGHT_SECONDS; } + @Override + public int hashCode() { + return super.hashCode() ^ getWidth(); + } + + @Override + public boolean equals(Object other) { + return other instanceof RepeaterFortnight && + ((Repeater) other).getType().equals(getType()) && + ((Repeater) other).getNow().equals(getNow()); + } + @Override public String toString() { return super.toString() + "-fortnight"; diff --git a/src/main/java/org/xbib/time/chronic/repeaters/RepeaterHour.java b/src/main/java/org/xbib/time/chronic/repeaters/RepeaterHour.java index a56bc2a..d44e78c 100644 --- a/src/main/java/org/xbib/time/chronic/repeaters/RepeaterHour.java +++ b/src/main/java/org/xbib/time/chronic/repeaters/RepeaterHour.java @@ -71,6 +71,18 @@ public class RepeaterHour extends RepeaterUnit { return RepeaterHour.HOUR_SECONDS; } + @Override + public int hashCode() { + return super.hashCode() ^ getWidth(); + } + + @Override + public boolean equals(Object other) { + return other instanceof RepeaterHour && + ((Repeater) other).getType().equals(getType()) && + ((Repeater) other).getNow().equals(getNow()); + } + @Override public String toString() { return super.toString() + "-hour"; diff --git a/src/main/java/org/xbib/time/chronic/repeaters/RepeaterMinute.java b/src/main/java/org/xbib/time/chronic/repeaters/RepeaterMinute.java index 1b06ef5..8e704a2 100644 --- a/src/main/java/org/xbib/time/chronic/repeaters/RepeaterMinute.java +++ b/src/main/java/org/xbib/time/chronic/repeaters/RepeaterMinute.java @@ -66,6 +66,18 @@ public class RepeaterMinute extends RepeaterUnit { return RepeaterMinute.MINUTE_SECONDS; } + @Override + public int hashCode() { + return super.hashCode() ^ getWidth(); + } + + @Override + public boolean equals(Object other) { + return other instanceof RepeaterMinute && + ((Repeater) other).getType().equals(getType()) && + ((Repeater) other).getNow().equals(getNow()); + } + @Override public String toString() { return super.toString() + "-minute"; diff --git a/src/main/java/org/xbib/time/chronic/repeaters/RepeaterMonth.java b/src/main/java/org/xbib/time/chronic/repeaters/RepeaterMonth.java index 4ec2a1e..608c700 100644 --- a/src/main/java/org/xbib/time/chronic/repeaters/RepeaterMonth.java +++ b/src/main/java/org/xbib/time/chronic/repeaters/RepeaterMonth.java @@ -66,6 +66,18 @@ public class RepeaterMonth extends RepeaterUnit { return RepeaterMonth.MONTH_SECONDS; } + @Override + public int hashCode() { + return super.hashCode() ^ getWidth(); + } + + @Override + public boolean equals(Object other) { + return other instanceof RepeaterMonth && + ((Repeater) other).getType().equals(getType()) && + ((Repeater) other).getNow().equals(getNow()); + } + @Override public String toString() { return super.toString() + "-month"; diff --git a/src/main/java/org/xbib/time/chronic/repeaters/RepeaterMonthName.java b/src/main/java/org/xbib/time/chronic/repeaters/RepeaterMonthName.java index 5602305..2ecb5fd 100644 --- a/src/main/java/org/xbib/time/chronic/repeaters/RepeaterMonthName.java +++ b/src/main/java/org/xbib/time/chronic/repeaters/RepeaterMonthName.java @@ -76,14 +76,14 @@ public class RepeaterMonthName extends Repeater { } else if (pointer == PointerType.NONE) { if (nowMonth <= targetMonth) { currentMonthBegin = ZonedDateTime.of(getNow().getYear(), targetMonth, 1, 0, 0, 0, 0, getNow().getZone()); - } else if (nowMonth > targetMonth) { + } else { currentMonthBegin = ZonedDateTime.of(getNow().getYear(), targetMonth, 1, 0, 0, 0, 0, getNow().getZone()) .plus(1, ChronoUnit.YEARS); } } else if (pointer == PointerType.PAST) { if (nowMonth > targetMonth) { currentMonthBegin = ZonedDateTime.of(getNow().getYear(), targetMonth, 1, 0, 0, 0, 0, getNow().getZone()); - } else if (nowMonth <= targetMonth) { + } else { currentMonthBegin = ZonedDateTime.of(getNow().getYear(), targetMonth, 1, 0, 0, 0, 0, getNow().getZone()) .minus(1, ChronoUnit.YEARS); } @@ -129,6 +129,18 @@ public class RepeaterMonthName extends Repeater { return RepeaterMonthName.MONTH_SECONDS; } + @Override + public int hashCode() { + return super.hashCode() ^ getWidth(); + } + + @Override + public boolean equals(Object other) { + return other instanceof RepeaterMonthName && + ((Repeater) other).getType().equals(getType()) && + ((Repeater) other).getNow().equals(getNow()); + } + @Override public String toString() { return super.toString() + "-monthname-" + getType(); diff --git a/src/main/java/org/xbib/time/chronic/repeaters/RepeaterSecond.java b/src/main/java/org/xbib/time/chronic/repeaters/RepeaterSecond.java index 5e4b753..12685d1 100644 --- a/src/main/java/org/xbib/time/chronic/repeaters/RepeaterSecond.java +++ b/src/main/java/org/xbib/time/chronic/repeaters/RepeaterSecond.java @@ -10,7 +10,7 @@ import java.time.temporal.ChronoUnit; * */ public class RepeaterSecond extends RepeaterUnit { - public static final int SECOND_SECONDS = 1; // (60 * 60); + private static final int SECOND_SECONDS = 1; private ZonedDateTime secondStart; @@ -42,6 +42,18 @@ public class RepeaterSecond extends RepeaterUnit { return RepeaterSecond.SECOND_SECONDS; } + @Override + public int hashCode() { + return super.hashCode() ^ getWidth(); + } + + @Override + public boolean equals(Object other) { + return other instanceof RepeaterSecond && + ((Repeater) other).getType().equals(getType()) && + ((Repeater) other).getNow().equals(getNow()); + } + @Override public String toString() { return super.toString() + "-second"; diff --git a/src/main/java/org/xbib/time/chronic/repeaters/RepeaterTime.java b/src/main/java/org/xbib/time/chronic/repeaters/RepeaterTime.java index 5e37fb4..fad681d 100644 --- a/src/main/java/org/xbib/time/chronic/repeaters/RepeaterTime.java +++ b/src/main/java/org/xbib/time/chronic/repeaters/RepeaterTime.java @@ -37,8 +37,8 @@ public class RepeaterTime extends Repeater { int minutesInSeconds = Integer.parseInt(t.substring(1)) * 60; type = new Tick(hoursInSeconds + minutesInSeconds, true); } else if (length == 4) { - boolean ambiguous = (time.contains(":") && Integer.parseInt(t.substring(0, 1)) != 0 && - Integer.parseInt(t.substring(0, 2)) <= 12); + boolean ambiguous = time.contains(":") && Integer.parseInt(t.substring(0, 1)) != 0 && + Integer.parseInt(t.substring(0, 2)) <= 12; int hours = Integer.parseInt(t.substring(0, 2)); int hoursInSeconds = hours * 60 * 60; int minutesInSeconds = Integer.parseInt(t.substring(2)) * 60; @@ -53,8 +53,8 @@ public class RepeaterTime extends Repeater { int seconds = Integer.parseInt(t.substring(3)); type = new Tick(hoursInSeconds + minutesInSeconds + seconds, true); } else if (length == 6) { - boolean ambiguous = (time.contains(":") && Integer.parseInt(t.substring(0, 1)) != 0 && - Integer.parseInt(t.substring(0, 2)) <= 12); + boolean ambiguous = time.contains(":") && Integer.parseInt(t.substring(0, 1)) != 0 && + Integer.parseInt(t.substring(0, 2)) <= 12; int hours = Integer.parseInt(t.substring(0, 2)); int hoursInSeconds = hours * 60 * 60; int minutesInSeconds = Integer.parseInt(t.substring(2, 4)) * 60; @@ -136,7 +136,7 @@ public class RepeaterTime extends Repeater { if (tick.isAmbiguous()) { List futureDates = new LinkedList<>(); futureDates.add(midnight.plus(tick.intValue(), ChronoUnit.SECONDS)); - futureDates.add(midnight.plus(halfDay + tick.intValue(), ChronoUnit.SECONDS)); + futureDates.add(midnight.plus(halfDay + (long) tick.intValue(), ChronoUnit.SECONDS)); futureDates.add(tomorrowMidnight.plus(tick.intValue(), ChronoUnit.SECONDS)); for (ZonedDateTime futureDate : futureDates) { if (futureDate.isAfter(now) || futureDate.equals(now)) { @@ -160,9 +160,9 @@ public class RepeaterTime extends Repeater { } else { if (tick.isAmbiguous()) { List pastDates = new LinkedList<>(); - pastDates.add(midnight.plus(halfDay + tick.intValue(), ChronoUnit.SECONDS)); + pastDates.add(midnight.plus(halfDay + (long) tick.intValue(), ChronoUnit.SECONDS)); pastDates.add(midnight.plus(tick.intValue(), ChronoUnit.SECONDS)); - pastDates.add(yesterdayMidnight.plus(tick.intValue() * 2, ChronoUnit.SECONDS)); + pastDates.add(yesterdayMidnight.plus(tick.intValue() * 2L, ChronoUnit.SECONDS)); for (ZonedDateTime pastDate : pastDates) { if (pastDate.isBefore(now) || pastDate.equals(now)) { currentTime = pastDate; @@ -190,8 +190,8 @@ public class RepeaterTime extends Repeater { } if (!first) { - int increment = (tick.isAmbiguous()) ? halfDay : fullDay; - int direction = (pointer == PointerType.FUTURE) ? 1 : -1; + int increment = tick.isAmbiguous() ? halfDay : fullDay; + long direction = pointer == PointerType.FUTURE ? 1L : -1L; currentTime = currentTime.plus(direction * increment, ChronoUnit.SECONDS); } @@ -200,10 +200,7 @@ public class RepeaterTime extends Repeater { @Override protected Span internalThisSpan(PointerType pointer) { - if (pointer == PointerType.NONE) { - pointer = PointerType.FUTURE; - } - return nextSpan(pointer); + return nextSpan(pointer == PointerType.NONE ? PointerType.FUTURE : pointer); } @Override @@ -216,6 +213,18 @@ public class RepeaterTime extends Repeater { return 1; } + @Override + public int hashCode() { + return super.hashCode() ^ getWidth(); + } + + @Override + public boolean equals(Object other) { + return other instanceof RepeaterTime && + ((Repeater) other).getType().equals(getType()) && + ((Repeater) other).getNow().equals(getNow()); + } + @Override public String toString() { return super.toString() + "-time-" + getType(); diff --git a/src/main/java/org/xbib/time/chronic/repeaters/RepeaterUnit.java b/src/main/java/org/xbib/time/chronic/repeaters/RepeaterUnit.java index 0e5484b..dd70bee 100644 --- a/src/main/java/org/xbib/time/chronic/repeaters/RepeaterUnit.java +++ b/src/main/java/org/xbib/time/chronic/repeaters/RepeaterUnit.java @@ -47,11 +47,23 @@ public abstract class RepeaterUnit extends Repeater { } } return null; - } catch (Throwable t) { - throw new RuntimeException("Failed to create RepeaterUnit.", t); + } catch (Exception e) { + throw new IllegalStateException("Failed to create RepeaterUnit", e); } } + @Override + public int hashCode() { + return super.hashCode() ^ getWidth(); + } + + @Override + public boolean equals(Object other) { + return other instanceof RepeaterUnit && + ((Repeater) other).getType().equals(getType()) && + ((Repeater) other).getNow().equals(getNow()); + } + /** * */ diff --git a/src/main/java/org/xbib/time/chronic/repeaters/RepeaterWeek.java b/src/main/java/org/xbib/time/chronic/repeaters/RepeaterWeek.java index f135f67..6022c5e 100644 --- a/src/main/java/org/xbib/time/chronic/repeaters/RepeaterWeek.java +++ b/src/main/java/org/xbib/time/chronic/repeaters/RepeaterWeek.java @@ -10,7 +10,7 @@ import java.time.temporal.ChronoUnit; * */ public class RepeaterWeek extends RepeaterUnit { - public static final int WEEK_SECONDS = 604800; // (7 * 24 * 60 * 60); + public static final int WEEK_SECONDS = 604800; public static final int WEEK_DAYS = 7; private ZonedDateTime currentWeekStart; @@ -38,7 +38,7 @@ public class RepeaterWeek extends RepeaterUnit { throw new IllegalArgumentException("Unable to handle pointer " + pointer + "."); } } else { - int direction = (pointer == PointerType.FUTURE) ? 1 : -1; + long direction = pointer == PointerType.FUTURE ? 1L : -1L; currentWeekStart = currentWeekStart.plus(RepeaterWeek.WEEK_DAYS * direction, ChronoUnit.DAYS); } @@ -88,6 +88,18 @@ public class RepeaterWeek extends RepeaterUnit { return RepeaterWeek.WEEK_SECONDS; } + @Override + public int hashCode() { + return super.hashCode() ^ getWidth(); + } + + @Override + public boolean equals(Object other) { + return other instanceof RepeaterWeek && + ((Repeater) other).getType().equals(getType()) && + ((Repeater) other).getNow().equals(getNow()); + } + @Override public String toString() { return super.toString() + "-week"; diff --git a/src/main/java/org/xbib/time/chronic/repeaters/RepeaterWeekend.java b/src/main/java/org/xbib/time/chronic/repeaters/RepeaterWeekend.java index a241ed5..8e460f8 100644 --- a/src/main/java/org/xbib/time/chronic/repeaters/RepeaterWeekend.java +++ b/src/main/java/org/xbib/time/chronic/repeaters/RepeaterWeekend.java @@ -16,25 +16,34 @@ public class RepeaterWeekend extends RepeaterUnit { @Override protected Span internalNextSpan(PointerType pointer) { - if (currentWeekStart == null) { - if (pointer == PointerType.FUTURE) { - RepeaterDayName saturdayRepeater = new RepeaterDayName(RepeaterDayName.DayName.SATURDAY); - saturdayRepeater.setNow(getNow()); - Span nextSaturdaySpan = saturdayRepeater.nextSpan(PointerType.FUTURE); - currentWeekStart = nextSaturdaySpan.getBeginCalendar(); - } else if (pointer == PointerType.PAST) { + if (currentWeekStart != null) { + long direction = pointer == PointerType.FUTURE ? 1L : -1L; + currentWeekStart = currentWeekStart.plus(direction * RepeaterWeek.WEEK_SECONDS, ChronoUnit.SECONDS); + ZonedDateTime c = currentWeekStart.plus(RepeaterWeekend.WEEKEND_SECONDS, ChronoUnit.SECONDS); + return new Span(currentWeekStart, c); + } + ZonedDateTime c; + switch (pointer) { + case PAST: { RepeaterDayName saturdayRepeater = new RepeaterDayName(RepeaterDayName.DayName.SATURDAY); saturdayRepeater.setNow(getNow().plus(RepeaterDay.DAY_SECONDS, ChronoUnit.SECONDS)); Span lastSaturdaySpan = saturdayRepeater.nextSpan(PointerType.PAST); currentWeekStart = lastSaturdaySpan.getBeginCalendar(); + c = currentWeekStart.plus(RepeaterWeekend.WEEKEND_SECONDS, ChronoUnit.SECONDS); + return new Span(currentWeekStart, c); } - } else { - long direction = pointer == PointerType.FUTURE ? 1L : -1L; - currentWeekStart = currentWeekStart.plus(direction * RepeaterWeek.WEEK_SECONDS, ChronoUnit.SECONDS); + case FUTURE: { + RepeaterDayName saturdayRepeater = new RepeaterDayName(RepeaterDayName.DayName.SATURDAY); + saturdayRepeater.setNow(getNow()); + Span nextSaturdaySpan = saturdayRepeater.nextSpan(PointerType.FUTURE); + currentWeekStart = nextSaturdaySpan.getBeginCalendar(); + c = currentWeekStart.plus(RepeaterWeekend.WEEKEND_SECONDS, ChronoUnit.SECONDS); + return new Span(currentWeekStart, c); + } + default: + break; } - assert currentWeekStart != null; - ZonedDateTime c = currentWeekStart.plus(RepeaterWeekend.WEEKEND_SECONDS, ChronoUnit.SECONDS); - return new Span(currentWeekStart, c); + throw new IllegalArgumentException("pointer type not expected"); } @Override @@ -73,6 +82,18 @@ public class RepeaterWeekend extends RepeaterUnit { return (int) RepeaterWeekend.WEEKEND_SECONDS; } + @Override + public int hashCode() { + return super.hashCode() ^ getWidth(); + } + + @Override + public boolean equals(Object other) { + return other instanceof RepeaterWeekend && + ((Repeater) other).getType().equals(getType()) && + ((Repeater) other).getNow().equals(getNow()); + } + @Override public String toString() { return super.toString() + "-weekend"; diff --git a/src/main/java/org/xbib/time/chronic/repeaters/RepeaterYear.java b/src/main/java/org/xbib/time/chronic/repeaters/RepeaterYear.java index 5feb588..f0e269e 100644 --- a/src/main/java/org/xbib/time/chronic/repeaters/RepeaterYear.java +++ b/src/main/java/org/xbib/time/chronic/repeaters/RepeaterYear.java @@ -70,6 +70,18 @@ public class RepeaterYear extends RepeaterUnit { return (365 * 24 * 60 * 60); } + @Override + public int hashCode() { + return super.hashCode() ^ getWidth(); + } + + @Override + public boolean equals(Object other) { + return other instanceof RepeaterYear && + ((Repeater) other).getType().equals(getType()) && + ((Repeater) other).getNow().equals(getNow()); + } + @Override public String toString() { return super.toString() + "-year"; diff --git a/src/main/java/org/xbib/time/format/Composite.java b/src/main/java/org/xbib/time/format/Composite.java new file mode 100644 index 0000000..89d2025 --- /dev/null +++ b/src/main/java/org/xbib/time/format/Composite.java @@ -0,0 +1,109 @@ +package org.xbib.time.format; + +import java.io.IOException; +import java.io.Writer; +import java.time.Period; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +/** + * Composite implementation that merges other fields to create a full pattern. + */ +class Composite implements PeriodPrinter, PeriodParser { + + private final PeriodPrinter[] iPrinters; + private final PeriodParser[] iParsers; + + Composite(List elementPairs) { + List printerList = new ArrayList<>(); + List parserList = new ArrayList<>(); + + decompose(elementPairs, printerList, parserList); + + if (printerList.isEmpty()) { + iPrinters = null; + } else { + iPrinters = printerList.toArray(new PeriodPrinter[printerList.size()]); + } + + if (parserList.isEmpty()) { + iParsers = null; + } else { + iParsers = parserList.toArray(new PeriodParser[parserList.size()]); + } + } + + public int countFieldsToPrint(Period period, int stopAt, Locale locale) { + int sum = 0; + PeriodPrinter[] printers = iPrinters; + for (int i = printers.length; sum < stopAt && --i >= 0; ) { + sum += printers[i].countFieldsToPrint(period, Integer.MAX_VALUE, locale); + } + return sum; + } + + public int calculatePrintedLength(Period period, Locale locale) { + int sum = 0; + PeriodPrinter[] printers = iPrinters; + for (int i = printers.length; --i >= 0; ) { + sum += printers[i].calculatePrintedLength(period, locale); + } + return sum; + } + + public void printTo(StringBuilder buf, Period period, Locale locale) { + for (PeriodPrinter printer : iPrinters) { + printer.printTo(buf, period, locale); + } + } + + public void printTo(Writer out, Period period, Locale locale) throws IOException { + for (PeriodPrinter printer : iPrinters) { + printer.printTo(out, period, locale); + } + } + + public int parseInto(PeriodAmount period, String periodStr, int pos, Locale locale) { + int position = pos; + PeriodParser[] parsers = iParsers; + if (parsers == null) { + throw new UnsupportedOperationException(); + } + int len = parsers.length; + for (int i = 0; i < len && position >= 0; i++) { + position = parsers[i].parseInto(period, periodStr, position, locale); + } + return position; + } + + private void decompose(List elementPairs, List printerList, List parserList) { + int size = elementPairs.size(); + for (int i = 0; i < size; i += 2) { + Object element = elementPairs.get(i); + if (element instanceof PeriodPrinter) { + if (element instanceof Composite) { + addArrayToList(printerList, ((Composite) element).iPrinters); + } else { + printerList.add(element); + } + } + + element = elementPairs.get(i + 1); + if (element instanceof PeriodParser) { + if (element instanceof Composite) { + addArrayToList(parserList, ((Composite) element).iParsers); + } else { + parserList.add(element); + } + } + } + } + + private void addArrayToList(List list, Object[] array) { + if (array != null) { + Collections.addAll(list, array); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/xbib/time/format/CompositeAffix.java b/src/main/java/org/xbib/time/format/CompositeAffix.java new file mode 100644 index 0000000..c3a485f --- /dev/null +++ b/src/main/java/org/xbib/time/format/CompositeAffix.java @@ -0,0 +1,82 @@ +package org.xbib.time.format; + +import java.io.IOException; +import java.io.Writer; +import java.util.HashSet; +import java.util.Set; + +/** + * Builds a composite affix by merging two other affix implementations. + */ +class CompositeAffix extends IgnorableAffix { + private final PeriodFieldAffix iLeft; + private final PeriodFieldAffix iRight; + private final String[] iLeftRightCombinations; + + CompositeAffix(PeriodFieldAffix left, PeriodFieldAffix right) { + iLeft = left; + iRight = right; + + // We need to construct all possible combinations of left and right. + // We are doing it once in constructor so that getAffixes() is quicker. + Set result = new HashSet(); + for (String leftText : iLeft.getAffixes()) { + for (String rightText : iRight.getAffixes()) { + result.add(leftText + rightText); + } + } + iLeftRightCombinations = result.toArray(new String[result.size()]); + } + + @Override + public int calculatePrintedLength(int value) { + return iLeft.calculatePrintedLength(value) + + iRight.calculatePrintedLength(value); + } + + @Override + public void printTo(StringBuilder buf, int value) { + iLeft.printTo(buf, value); + iRight.printTo(buf, value); + } + + @Override + public void printTo(Writer out, int value) throws IOException { + iLeft.printTo(out, value); + iRight.printTo(out, value); + } + + @Override + public int parse(String periodStr, int position) { + int pos = iLeft.parse(periodStr, position); + if (pos >= 0) { + pos = iRight.parse(periodStr, pos); + if (pos >= 0 && matchesOtherAffix(parse(periodStr, pos) - pos, periodStr, position)) { + return ~position; + } + } + return pos; + } + + @Override + public int scan(String periodStr, final int position) { + int leftPosition = iLeft.scan(periodStr, position); + if (leftPosition >= 0) { + int rightPosition = iRight.scan(periodStr, iLeft.parse(periodStr, leftPosition)); + if (!(rightPosition >= 0 && matchesOtherAffix(iRight.parse(periodStr, rightPosition) - + leftPosition, periodStr, position))) { + if (leftPosition > 0) { + return leftPosition; + } else { + return rightPosition; + } + } + } + return ~position; + } + + @Override + public String[] getAffixes() { + return iLeftRightCombinations.clone(); + } +} diff --git a/src/main/java/org/xbib/time/format/FieldFormatter.java b/src/main/java/org/xbib/time/format/FieldFormatter.java new file mode 100644 index 0000000..da88774 --- /dev/null +++ b/src/main/java/org/xbib/time/format/FieldFormatter.java @@ -0,0 +1,303 @@ +package org.xbib.time.format; + +import java.io.IOException; +import java.io.Writer; +import java.time.Period; +import java.time.temporal.ChronoUnit; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +/** + * Formats the numeric value of a field, potentially with prefix/suffix. + */ +class FieldFormatter implements PeriodPrinter, PeriodParser { + private final int iMinPrintedDigits; + //private final int iPrintZeroSetting; + private final int iMaxParsedDigits; + private final boolean iRejectSignedValues; + + /** + * The index of the field type, 0=year, etc. + */ + private final ChronoUnit unit; + /** + * The array of the latest formatter added for each type. + * This is shared between all the field formatters in a formatter. + */ + //private final FieldFormatter[] iFieldFormatters; + + private final PeriodFieldAffix iPrefix; + private final PeriodFieldAffix iSuffix; + + FieldFormatter(int minPrintedDigits, + int maxParsedDigits, boolean rejectSignedValues, + ChronoUnit chronoUnit, + PeriodFieldAffix prefix, PeriodFieldAffix suffix) { + iMinPrintedDigits = minPrintedDigits; + iMaxParsedDigits = maxParsedDigits; + iRejectSignedValues = rejectSignedValues; + this.unit = chronoUnit; + iPrefix = prefix; + iSuffix = suffix; + } + + FieldFormatter(FieldFormatter field, PeriodFieldAffix periodFieldAffix) { + PeriodFieldAffix suffix = periodFieldAffix; + iMinPrintedDigits = field.iMinPrintedDigits; + iMaxParsedDigits = field.iMaxParsedDigits; + iRejectSignedValues = field.iRejectSignedValues; + this.unit = field.unit; + iPrefix = field.iPrefix; + if (field.iSuffix != null) { + suffix = new CompositeAffix(field.iSuffix, suffix); + } + iSuffix = suffix; + } + + public void finish(FieldFormatter[] fieldFormatters) { + Set prefixesToIgnore = new HashSet<>(); + Set suffixesToIgnore = new HashSet<>(); + for (FieldFormatter fieldFormatter : fieldFormatters) { + if (fieldFormatter != null && !this.equals(fieldFormatter)) { + prefixesToIgnore.add(fieldFormatter.iPrefix); + suffixesToIgnore.add(fieldFormatter.iSuffix); + } + } + // if we have a prefix then allow ignore behaviour + if (iPrefix != null) { + iPrefix.finish(prefixesToIgnore); + } + // if we have a suffix then allow ignore behaviour + if (iSuffix != null) { + iSuffix.finish(suffixesToIgnore); + } + } + + public int countFieldsToPrint(Period period, int stopAt, Locale locale) { + if (stopAt <= 0) { + return 0; + } + if (getFieldValue(period) != Long.MAX_VALUE) { + return 1; + } + return 0; + } + + public int calculatePrintedLength(Period period, Locale locale) { + long valueLong = getFieldValue(period); + if (valueLong == Long.MAX_VALUE) { + return 0; + } + + int sum = Math.max(FormatUtils.calculateDigitCount(valueLong), iMinPrintedDigits); + int value = (int) valueLong; + + if (iPrefix != null) { + sum += iPrefix.calculatePrintedLength(value); + } + if (iSuffix != null) { + sum += iSuffix.calculatePrintedLength(value); + } + + return sum; + } + + public void printTo(StringBuilder buf, Period period, Locale locale) { + long valueLong = getFieldValue(period); + if (valueLong == Long.MAX_VALUE) { + return; + } + int value = (int) valueLong; + if (iPrefix != null) { + iPrefix.printTo(buf, value); + } + int minDigits = iMinPrintedDigits; + if (minDigits <= 1) { + FormatUtils.appendUnpaddedInteger(buf, value); + } else { + FormatUtils.appendPaddedInteger(buf, value, minDigits); + } + if (iSuffix != null) { + iSuffix.printTo(buf, value); + } + } + + public void printTo(Writer out, Period period, Locale locale) throws IOException { + long valueLong = getFieldValue(period); + if (valueLong == Long.MAX_VALUE) { + return; + } + int value = (int) valueLong; + if (iPrefix != null) { + iPrefix.printTo(out, value); + } + int minDigits = iMinPrintedDigits; + if (minDigits <= 1) { + FormatUtils.writeUnpaddedInteger(out, value); + } else { + FormatUtils.writePaddedInteger(out, value, minDigits); + } + if (iSuffix != null) { + iSuffix.printTo(out, value); + } + } + + public int parseInto(PeriodAmount period, String text, int pos, Locale locale) { + int position = pos; + if (position >= text.length()) { + return ~position; + } + + if (iPrefix != null) { + position = iPrefix.parse(text, position); + if (position < 0) { + return position; + } + } + int limit; + limit = Math.min(iMaxParsedDigits, text.length() - position); + int length = 0; + boolean hasDigits = false; + boolean negative; + while (length < limit) { + char c = text.charAt(position + length); + if (length == 0 && (c == '-' || c == '+') && !iRejectSignedValues) { + negative = c == '-'; + if (length + 1 >= limit || + (c = text.charAt(position + length + 1)) < '0' || c > '9') { + break; + } + if (negative) { + length++; + } else { + position++; + } + limit = Math.min(limit + 1, text.length() - position); + continue; + } + if (c >= '0' && c <= '9') { + hasDigits = true; + } else { + break; + } + length++; + } + + if (!hasDigits) { + return ~position; + } + setFieldValue(period, unit, parseInt(text, position, length)); + position += length; + if (position >= 0 && iSuffix != null) { + position = iSuffix.parse(text, position); + } + return position; + } + + /** + * @param text text to parse + * @param pos position in text + * @param len exact count of characters to parse + * @return parsed int value + */ + private int parseInt(String text, int pos, int len) { + int position = pos; + int length = len; + if (length >= 10) { + return Integer.parseInt(text.substring(position, position + length)); + } + if (length <= 0) { + return 0; + } + int value = text.charAt(position++); + length--; + boolean negative; + if (value == '-') { + if (--length < 0) { + return 0; + } + negative = true; + value = text.charAt(position++); + } else { + negative = false; + } + value -= '0'; + while (length-- > 0) { + value = ((value << 3) + (value << 1)) + text.charAt(position++) - '0'; + } + return negative ? -value : value; + } + + /** + * @return Long.MAX_VALUE if nothing to print, otherwise value + */ + long getFieldValue(Period period) { + long value; + switch (unit) { + default: + return Long.MAX_VALUE; + case YEARS: + value = period.get(ChronoUnit.YEARS); + break; + case MONTHS: + value = period.get(ChronoUnit.MONTHS); + break; + case WEEKS: + value = period.get(ChronoUnit.WEEKS); + break; + case DAYS: + value = period.get(ChronoUnit.DAYS); + break; + case HOURS: + value = period.get(ChronoUnit.HOURS); + break; + case MINUTES: + value = period.get(ChronoUnit.MINUTES); + break; + case SECONDS: + value = period.get(ChronoUnit.SECONDS); + break; + case MILLIS: + value = period.get(ChronoUnit.MILLIS); + break; + } + + return value; + } + + void setFieldValue(PeriodAmount period, ChronoUnit field, long value) { + switch (field) { + default: + break; + case YEARS: + period.set(ChronoUnit.YEARS, value); + break; + case MONTHS: + period.set(ChronoUnit.MONTHS, value); + break; + case WEEKS: + period.set(ChronoUnit.WEEKS, value); + break; + case DAYS: + period.set(ChronoUnit.DAYS, value); + break; + case HOURS: + period.set(ChronoUnit.HOURS, value); + break; + case MINUTES: + period.set(ChronoUnit.MINUTES, value); + break; + case SECONDS: + period.set(ChronoUnit.SECONDS, value); + break; + case MILLIS: + period.set(ChronoUnit.MILLIS, value); + break; + } + } + + ChronoUnit getFieldType() { + return unit; + } +} diff --git a/src/main/java/org/xbib/time/format/FormatUtils.java b/src/main/java/org/xbib/time/format/FormatUtils.java index dcd1ded..43f0bbc 100644 --- a/src/main/java/org/xbib/time/format/FormatUtils.java +++ b/src/main/java/org/xbib/time/format/FormatUtils.java @@ -2,6 +2,8 @@ package org.xbib.time.format; import java.io.IOException; import java.io.Writer; +import java.util.logging.Level; +import java.util.logging.Logger; /** * Utility methods used by formatters. @@ -9,6 +11,8 @@ import java.io.Writer; */ public class FormatUtils { + private static final Logger logger = Logger.getLogger(FormatUtils.class.getName()); + private static final double LOG_10 = Math.log(10); /** @@ -31,7 +35,7 @@ public class FormatUtils { try { appendPaddedInteger((Appendable) buf, value, size); } catch (IOException e) { - // StringBuilder does not throw IOException + logger.log(Level.FINE, e.getMessage(), e); } } @@ -42,11 +46,13 @@ public class FormatUtils { * This method is optimized for converting small values to strings. * * @param appenadble receives integer converted to a string - * @param value value to convert to a string - * @param size minimum amount of digits to append + * @param intvalue value to convert to a string + * @param digits minimum amount of digits to append * @throws IOException exception */ - public static void appendPaddedInteger(Appendable appenadble, int value, int size) throws IOException { + public static void appendPaddedInteger(Appendable appenadble, int intvalue, int digits) throws IOException { + int value = intvalue; + int size = digits; if (value < 0) { appenadble.append('-'); if (value != Integer.MIN_VALUE) { @@ -55,7 +61,8 @@ public class FormatUtils { for (; size > 10; size--) { appenadble.append('0'); } - appenadble.append("" + -(long) Integer.MIN_VALUE); + appenadble.append(Long.toString(Integer.MIN_VALUE)); + //.append("" + -(long) Integer.MIN_VALUE) return; } } @@ -76,15 +83,15 @@ public class FormatUtils { // Append remainder by calculating (value - d * 10). appenadble.append((char) (value - (d << 3) - (d << 1) + '0')); } else { - int digits; + int d; if (value < 1000) { - digits = 3; + d = 3; } else if (value < 10000) { - digits = 4; + d = 4; } else { - digits = (int) (Math.log(value) / LOG_10) + 1; + d = (int) (Math.log(value) / LOG_10) + 1; } - for (; size > digits; size--) { + for (; size > d; size--) { appenadble.append('0'); } appenadble.append(Integer.toString(value)); @@ -105,7 +112,7 @@ public class FormatUtils { try { appendPaddedInteger((Appendable) buf, value, size); } catch (IOException e) { - // StringBuilder does not throw IOException + logger.log(Level.FINE, e.getMessage(), e); } } @@ -116,12 +123,14 @@ public class FormatUtils { * This method is optimized for converting small values to strings. * * @param appendable receives integer converted to a string - * @param value value to convert to a string - * @param size minimum amount of digits to append + * @param longvalue value to convert to a string + * @param digits minimum amount of digits to append * @throws IOException exception */ - public static void appendPaddedInteger(Appendable appendable, long value, int size) throws IOException { + public static void appendPaddedInteger(Appendable appendable, long longvalue, int digits) throws IOException { + long value = longvalue; int intValue = (int) value; + int size = digits; if (intValue == value) { appendPaddedInteger(appendable, intValue, size); } else if (size <= 19) { @@ -139,8 +148,8 @@ public class FormatUtils { return; } } - int digits = (int) (Math.log(value) / LOG_10) + 1; - for (; size > digits; size--) { + int d = (int) (Math.log(value) / LOG_10) + 1; + for (; size > d; size--) { appendable.append('0'); } appendable.append(Long.toString(value)); @@ -154,12 +163,14 @@ public class FormatUtils { * This method is optimized for converting small values to strings. * * @param out receives integer converted to a string - * @param value value to convert to a string - * @param size minimum amount of digits to append + * @param val value to convert to a string + * @param digits minimum amount of digits to append * @throws IOException exception */ - public static void writePaddedInteger(Writer out, int value, int size) + public static void writePaddedInteger(Writer out, int val, int digits) throws IOException { + int value = val; + int size = digits; if (value < 0) { out.write('-'); if (value != Integer.MIN_VALUE) { @@ -168,7 +179,8 @@ public class FormatUtils { for (; size > 10; size--) { out.write('0'); } - out.write("" + -(long) Integer.MIN_VALUE); + //out.write("" + -(long) Integer.MIN_VALUE); + out.write(Long.toString(Integer.MIN_VALUE)); return; } } @@ -189,15 +201,15 @@ public class FormatUtils { // Append remainder by calculating (value - d * 10). out.write(value - (d << 3) - (d << 1) + '0'); } else { - int digits; + int d; if (value < 1000) { - digits = 3; + d = 3; } else if (value < 10000) { - digits = 4; + d = 4; } else { - digits = (int) (Math.log(value) / LOG_10) + 1; + d = (int) (Math.log(value) / LOG_10) + 1; } - for (; size > digits; size--) { + for (; size > d; size--) { out.write('0'); } out.write(Integer.toString(value)); @@ -211,12 +223,13 @@ public class FormatUtils { * This method is optimized for converting small values to strings. * * @param out receives integer converted to a string - * @param value value to convert to a string - * @param size minimum amount of digits to append + * @param longvalue value to convert to a string + * @param digits minimum amount of digits to append * @throws IOException exception */ - public static void writePaddedInteger(Writer out, long value, int size) - throws IOException { + public static void writePaddedInteger(Writer out, long longvalue, int digits) throws IOException { + long value = longvalue; + int size = digits; int intValue = (int) value; if (intValue == value) { writePaddedInteger(out, intValue, size); @@ -235,8 +248,8 @@ public class FormatUtils { return; } } - int digits = (int) (Math.log(value) / LOG_10) + 1; - for (; size > digits; size--) { + int d = (int) (Math.log(value) / LOG_10) + 1; + for (; size > d; size--) { out.write('0'); } out.write(Long.toString(value)); @@ -254,7 +267,7 @@ public class FormatUtils { try { appendUnpaddedInteger((Appendable) buf, value); } catch (IOException e) { - // StringBuilder do not throw IOException + logger.log(Level.FINE, e.getMessage(), e); } } @@ -264,16 +277,17 @@ public class FormatUtils { * This method is optimized for converting small values to strings. * * @param appendable receives integer converted to a string - * @param value value to convert to a string + * @param val value to convert to a string * @throws IOException exception */ - public static void appendUnpaddedInteger(Appendable appendable, int value) throws IOException { + public static void appendUnpaddedInteger(Appendable appendable, int val) throws IOException { + int value = val; if (value < 0) { appendable.append('-'); if (value != Integer.MIN_VALUE) { value = -value; } else { - appendable.append("" + -(long) Integer.MIN_VALUE); + appendable.append(Long.toString(Integer.MIN_VALUE)); return; } } @@ -303,7 +317,7 @@ public class FormatUtils { try { appendUnpaddedInteger((Appendable) buf, value); } catch (IOException e) { - // StringBuilder do not throw IOException + logger.log(Level.FINE, e.getMessage(), e); } } @@ -331,17 +345,18 @@ public class FormatUtils { * This method is optimized for converting small values to strings. * * @param out receives integer converted to a string - * @param value value to convert to a string + * @param val value to convert to a string * @throws IOException exception */ - public static void writeUnpaddedInteger(Writer out, int value) + public static void writeUnpaddedInteger(Writer out, int val) throws IOException { + int value = val; if (value < 0) { out.write('-'); if (value != Integer.MIN_VALUE) { value = -value; } else { - out.write("" + -(long) Integer.MIN_VALUE); + out.write(Long.toString(Integer.MIN_VALUE)); return; } } @@ -394,12 +409,8 @@ public class FormatUtils { return 20; } } - return - (value < 10 ? 1 : - (value < 100 ? 2 : - (value < 1000 ? 3 : - (value < 10000 ? 4 : - ((int) (Math.log(value) / LOG_10) + 1))))); + return value < 10 ? 1 : value < 100 ? 2 : value < 1000 ? 3 : value < 10000 ? 4 : + ((int) (Math.log(value) / LOG_10) + 1); } static int parseTwoDigits(CharSequence text, int position) { diff --git a/src/main/java/org/xbib/time/format/IgnorableAffix.java b/src/main/java/org/xbib/time/format/IgnorableAffix.java new file mode 100644 index 0000000..96a0b9a --- /dev/null +++ b/src/main/java/org/xbib/time/format/IgnorableAffix.java @@ -0,0 +1,69 @@ +package org.xbib.time.format; + +import java.util.HashSet; +import java.util.Set; + +/** + * An affix that can be ignored. + */ +abstract class IgnorableAffix implements PeriodFieldAffix { + private volatile String[] iOtherAffixes; + + public void finish(Set periodFieldAffixesToIgnore) { + if (iOtherAffixes == null) { + // Calculate the shortest affix in this instance. + int shortestAffixLength = Integer.MAX_VALUE; + String shortestAffix = null; + for (String affix : getAffixes()) { + if (affix.length() < shortestAffixLength) { + shortestAffixLength = affix.length(); + shortestAffix = affix; + } + } + + // Pick only affixes that are longer than the shortest affix in this instance. + // This will reduce the number of parse operations and thus speed up the PeriodFormatter. + // also need to pick affixes that differ only in case (but not those that are identical) + Set affixesToIgnore = new HashSet(); + for (PeriodFieldAffix periodFieldAffixToIgnore : periodFieldAffixesToIgnore) { + if (periodFieldAffixToIgnore != null) { + for (String affixToIgnore : periodFieldAffixToIgnore.getAffixes()) { + if (affixToIgnore.length() > shortestAffixLength || + (affixToIgnore.equalsIgnoreCase(shortestAffix) && + !affixToIgnore.equals(shortestAffix))) { + affixesToIgnore.add(affixToIgnore); + } + } + } + } + iOtherAffixes = affixesToIgnore.toArray(new String[affixesToIgnore.size()]); + } + } + + /** + * Checks if there is a match among the other affixes (stored internally) + * that is longer than the passed value (textLength). + * + * @param textLength the length of the match + * @param periodStr the Period string that will be parsed + * @param position the position in the Period string at which the parsing should be started. + * @return true if the other affixes (stored internally) contain a match + * that is longer than the textLength parameter, false otherwise + */ + protected boolean matchesOtherAffix(int textLength, String periodStr, int position) { + if (iOtherAffixes != null) { + // ignore case when affix length differs + // match case when affix length is same + for (String affixToIgnore : iOtherAffixes) { + int textToIgnoreLength = affixToIgnore.length(); + if ((textLength < textToIgnoreLength && + periodStr.regionMatches(true, position, affixToIgnore, 0, textToIgnoreLength)) || + (textLength == textToIgnoreLength && + periodStr.regionMatches(false, position, affixToIgnore, 0, textToIgnoreLength))) { + return true; + } + } + } + return false; + } +} diff --git a/src/main/java/org/xbib/time/format/Literal.java b/src/main/java/org/xbib/time/format/Literal.java new file mode 100644 index 0000000..88c2455 --- /dev/null +++ b/src/main/java/org/xbib/time/format/Literal.java @@ -0,0 +1,42 @@ +package org.xbib.time.format; + +import java.io.IOException; +import java.io.Writer; +import java.time.Period; +import java.util.Locale; + +/** + * Handles a simple literal piece of text. + */ +class Literal implements PeriodPrinter, PeriodParser { + static final Literal EMPTY = new Literal(""); + private final String iText; + + Literal(String text) { + iText = text; + } + + public int countFieldsToPrint(Period period, int stopAt, Locale locale) { + return 0; + } + + public int calculatePrintedLength(Period period, Locale locale) { + return iText.length(); + } + + public void printTo(StringBuilder buf, Period period, Locale locale) { + buf.append(iText); + } + + public void printTo(Writer out, Period period, Locale locale) throws IOException { + out.write(iText); + } + + public int parseInto(PeriodAmount period, String periodStr, + int position, Locale locale) { + if (periodStr.regionMatches(true, position, iText, 0, iText.length())) { + return position + iText.length(); + } + return ~position; + } +} diff --git a/src/main/java/org/xbib/time/format/PeriodFieldAffix.java b/src/main/java/org/xbib/time/format/PeriodFieldAffix.java new file mode 100644 index 0000000..d336d87 --- /dev/null +++ b/src/main/java/org/xbib/time/format/PeriodFieldAffix.java @@ -0,0 +1,46 @@ +package org.xbib.time.format; + +import java.io.IOException; +import java.io.Writer; +import java.util.Set; + +/** + * Defines a formatted field's prefix or suffix text. + * This can be used for fields such as 'n hours' or 'nH' or 'Hour:n'. + */ +interface PeriodFieldAffix { + + int calculatePrintedLength(int value); + + void printTo(StringBuilder buf, int value); + + void printTo(Writer out, int value) throws IOException; + + /** + * @return new position after parsing affix, or ~position of failure + */ + int parse(String periodStr, int position); + + /** + * @return position where affix starts, or original ~position if not found + */ + int scan(String periodStr, int position); + + /** + * @return a copy of array of affixes + */ + String[] getAffixes(); + + /** + * This method should be called only once. + * After first call consecutive calls to this methods will have no effect. + * Causes this affix to ignore a match (parse and scan + * methods) if there is an affix in the passed list that holds + * affix text which satisfy both following conditions: + * - the affix text is also a match + * - the affix text is longer than the match from this object + * + * @param affixesToIgnore affixes to ignore + */ + void finish(Set affixesToIgnore); +} diff --git a/src/main/java/org/xbib/time/format/PeriodFormatterBuilder.java b/src/main/java/org/xbib/time/format/PeriodFormatterBuilder.java index 59f1d4f..3d5e349 100644 --- a/src/main/java/org/xbib/time/format/PeriodFormatterBuilder.java +++ b/src/main/java/org/xbib/time/format/PeriodFormatterBuilder.java @@ -1,22 +1,8 @@ package org.xbib.time.format; -import java.io.IOException; -import java.io.Writer; -import java.time.Period; import java.time.temporal.ChronoUnit; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashSet; import java.util.List; -import java.util.Locale; -import java.util.Set; -import java.util.TreeSet; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.regex.Pattern; - /** * Factory that creates complex instances of PeriodFormatter via method calls. @@ -46,8 +32,6 @@ import java.util.regex.Pattern; */ public class PeriodFormatterBuilder { - private static final ConcurrentMap PATTERNS = new ConcurrentHashMap<>(); - private int iMinPrintedDigits; private int iMaxParsedDigits; private boolean iRejectSignedValues; @@ -347,11 +331,12 @@ public class PeriodFormatterBuilder { * Append a field prefix which applies only to the next appended field. If * the field is not printed, neither is the prefix. * - * @param prefix custom prefix + * @param periodFieldAffix custom prefix * @return this PeriodFormatterBuilder * @see #appendSuffix */ - private PeriodFormatterBuilder appendPrefix(PeriodFieldAffix prefix) { + private PeriodFormatterBuilder appendPrefix(PeriodFieldAffix periodFieldAffix) { + PeriodFieldAffix prefix = periodFieldAffix; if (prefix == null) { throw new IllegalArgumentException(); } @@ -565,7 +550,7 @@ public class PeriodFormatterBuilder { private PeriodFormatterBuilder appendSuffix(PeriodFieldAffix suffix) { final Object originalPrinter; final Object originalParser; - if (iElementPairs.size() > 0) { + if (!iElementPairs.isEmpty()) { originalPrinter = iElementPairs.get(iElementPairs.size() - 2); originalParser = iElementPairs.get(iElementPairs.size() - 1); } else { @@ -690,7 +675,7 @@ public class PeriodFormatterBuilder { // optimise zero formatter case List pairs = iElementPairs; - if (pairs.size() == 0) { + if (pairs.isEmpty()) { if (useAfter && !useBefore) { Separator separator = new Separator(text, finalText, variants, Literal.EMPTY, Literal.EMPTY, false, true); @@ -712,7 +697,7 @@ public class PeriodFormatterBuilder { } // merge formatters - if (lastSeparator != null && pairs.size() == 0) { + if (lastSeparator != null && pairs.isEmpty()) { throw new IllegalStateException("Cannot have two adjacent separators"); } else { Object[] comp = createComposite(pairs); @@ -742,1037 +727,4 @@ public class PeriodFormatterBuilder { iNotParser |= (parser == null); return this; } - - /** - * Defines a formatted field's prefix or suffix text. - * This can be used for fields such as 'n hours' or 'nH' or 'Hour:n'. - */ - interface PeriodFieldAffix { - - int calculatePrintedLength(int value); - - void printTo(StringBuilder buf, int value); - - void printTo(Writer out, int value) throws IOException; - - /** - * @return new position after parsing affix, or ~position of failure - */ - int parse(String periodStr, int position); - - /** - * @return position where affix starts, or original ~position if not found - */ - int scan(String periodStr, int position); - - /** - * @return a copy of array of affixes - */ - String[] getAffixes(); - - /** - * This method should be called only once. - * After first call consecutive calls to this methods will have no effect. - * Causes this affix to ignore a match (parse and scan - * methods) if there is an affix in the passed list that holds - * affix text which satisfy both following conditions: - * - the affix text is also a match - * - the affix text is longer than the match from this object - * - * @param affixesToIgnore affixes to ignore - */ - void finish(Set affixesToIgnore); - } - - /** - * An affix that can be ignored. - */ - abstract static class IgnorableAffix implements PeriodFieldAffix { - private volatile String[] iOtherAffixes; - - public void finish(Set periodFieldAffixesToIgnore) { - if (iOtherAffixes == null) { - // Calculate the shortest affix in this instance. - int shortestAffixLength = Integer.MAX_VALUE; - String shortestAffix = null; - for (String affix : getAffixes()) { - if (affix.length() < shortestAffixLength) { - shortestAffixLength = affix.length(); - shortestAffix = affix; - } - } - - // Pick only affixes that are longer than the shortest affix in this instance. - // This will reduce the number of parse operations and thus speed up the PeriodFormatter. - // also need to pick affixes that differ only in case (but not those that are identical) - Set affixesToIgnore = new HashSet(); - for (PeriodFieldAffix periodFieldAffixToIgnore : periodFieldAffixesToIgnore) { - if (periodFieldAffixToIgnore != null) { - for (String affixToIgnore : periodFieldAffixToIgnore.getAffixes()) { - if (affixToIgnore.length() > shortestAffixLength || - (affixToIgnore.equalsIgnoreCase(shortestAffix) && - !affixToIgnore.equals(shortestAffix))) { - affixesToIgnore.add(affixToIgnore); - } - } - } - } - iOtherAffixes = affixesToIgnore.toArray(new String[affixesToIgnore.size()]); - } - } - - /** - * Checks if there is a match among the other affixes (stored internally) - * that is longer than the passed value (textLength). - * - * @param textLength the length of the match - * @param periodStr the Period string that will be parsed - * @param position the position in the Period string at which the parsing should be started. - * @return true if the other affixes (stored internally) contain a match - * that is longer than the textLength parameter, false otherwise - */ - protected boolean matchesOtherAffix(int textLength, String periodStr, int position) { - if (iOtherAffixes != null) { - // ignore case when affix length differs - // match case when affix length is same - for (String affixToIgnore : iOtherAffixes) { - int textToIgnoreLength = affixToIgnore.length(); - if ((textLength < textToIgnoreLength && - periodStr.regionMatches(true, position, affixToIgnore, 0, textToIgnoreLength)) || - (textLength == textToIgnoreLength && - periodStr.regionMatches(false, position, affixToIgnore, 0, textToIgnoreLength))) { - return true; - } - } - } - return false; - } - } - - /** - * Implements an affix where the text does not vary by the amount. - */ - static class SimpleAffix extends IgnorableAffix { - private final String iText; - - SimpleAffix(String text) { - iText = text; - } - - public int calculatePrintedLength(int value) { - return iText.length(); - } - - public void printTo(StringBuilder buf, int value) { - buf.append(iText); - } - - public void printTo(Writer out, int value) throws IOException { - out.write(iText); - } - - public int parse(String periodStr, int position) { - String text = iText; - int textLength = text.length(); - if (periodStr.regionMatches(true, position, text, 0, textLength) && - !matchesOtherAffix(textLength, periodStr, position)) { - return position + textLength; - } - return ~position; - } - - public int scan(String periodStr, final int position) { - String text = iText; - int textLength = text.length(); - int sourceLength = periodStr.length(); - search: - for (int pos = position; pos < sourceLength; pos++) { - if (periodStr.regionMatches(true, pos, text, 0, textLength) && - !matchesOtherAffix(textLength, periodStr, pos)) { - return pos; - } - // Only allow number characters to be skipped in search of suffix. - switch (periodStr.charAt(pos)) { - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - case '.': - case ',': - case '+': - case '-': - break; - default: - break search; - } - } - return ~position; - } - - public String[] getAffixes() { - return new String[]{iText}; - } - } - - /** - * Implements an affix where the text varies by the amount of the field. - * Only singular (1) and plural (not 1) are supported. - */ - private static class PluralAffix extends IgnorableAffix { - private final String iSingularText; - private final String iPluralText; - - PluralAffix(String singularText, String pluralText) { - iSingularText = singularText; - iPluralText = pluralText; - } - - public int calculatePrintedLength(int value) { - return (value == 1 ? iSingularText : iPluralText).length(); - } - - public void printTo(StringBuilder buf, int value) { - buf.append(value == 1 ? iSingularText : iPluralText); - } - - public void printTo(Writer out, int value) throws IOException { - out.write(value == 1 ? iSingularText : iPluralText); - } - - public int parse(String periodStr, int position) { - String text1 = iPluralText; - String text2 = iSingularText; - if (text1.length() < text2.length()) { - // Swap in order to match longer one first. - String temp = text1; - text1 = text2; - text2 = temp; - } - if (periodStr.regionMatches(true, position, text1, 0, text1.length()) && - !matchesOtherAffix(text1.length(), periodStr, position)) { - return position + text1.length(); - } - if (periodStr.regionMatches(true, position, text2, 0, text2.length()) && - !matchesOtherAffix(text2.length(), periodStr, position)) { - return position + text2.length(); - } - return ~position; - } - - public int scan(String periodStr, final int position) { - String text1 = iPluralText; - String text2 = iSingularText; - - if (text1.length() < text2.length()) { - // Swap in order to match longer one first. - String temp = text1; - text1 = text2; - text2 = temp; - } - - int textLength1 = text1.length(); - int textLength2 = text2.length(); - - int sourceLength = periodStr.length(); - for (int pos = position; pos < sourceLength; pos++) { - if (periodStr.regionMatches(true, pos, text1, 0, textLength1) && - !matchesOtherAffix(text1.length(), periodStr, pos)) { - return pos; - } - if (periodStr.regionMatches(true, pos, text2, 0, textLength2) && - !matchesOtherAffix(text2.length(), periodStr, pos)) { - return pos; - } - } - return ~position; - } - - public String[] getAffixes() { - return new String[]{iSingularText, iPluralText}; - } - } - - /** - * Implements an affix where the text varies by the amount of the field. - * Different amounts are supported based on the provided parameters. - */ - private static class RegExAffix extends IgnorableAffix { - private static final Comparator LENGTH_DESC_COMPARATOR = new Comparator() { - public int compare(String o1, String o2) { - return o2.length() - o1.length(); - } - }; - - private final String[] iSuffixes; - private final Pattern[] iPatterns; - - // The parse method has to iterate over the suffixes from the longest one to the shortest one - // Otherwise it might consume not enough characters. - private final String[] iSuffixesSortedDescByLength; - - RegExAffix(String[] regExes, String[] texts) { - iSuffixes = texts.clone(); - iPatterns = new Pattern[regExes.length]; - for (int i = 0; i < regExes.length; i++) { - Pattern pattern = PATTERNS.get(regExes[i]); - if (pattern == null) { - pattern = Pattern.compile(regExes[i]); - Pattern p = PATTERNS.putIfAbsent(regExes[i], pattern); - } - iPatterns[i] = pattern; - } - iSuffixesSortedDescByLength = iSuffixes.clone(); - Arrays.sort(iSuffixesSortedDescByLength, LENGTH_DESC_COMPARATOR); - } - - private int selectSuffixIndex(int value) { - String valueString = String.valueOf(value); - for (int i = 0; i < iPatterns.length; i++) { - if (iPatterns[i].matcher(valueString).matches()) { - return i; - } - } - return iPatterns.length - 1; - } - - @Override - public int calculatePrintedLength(int value) { - return iSuffixes[selectSuffixIndex(value)].length(); - } - - @Override - public void printTo(StringBuilder buf, int value) { - buf.append(iSuffixes[selectSuffixIndex(value)]); - } - - @Override - public void printTo(Writer out, int value) throws IOException { - out.write(iSuffixes[selectSuffixIndex(value)]); - } - - @Override - public int parse(String periodStr, int position) { - for (String text : iSuffixesSortedDescByLength) { - if (periodStr.regionMatches(true, position, text, 0, text.length()) && - !matchesOtherAffix(text.length(), periodStr, position)) { - return position + text.length(); - } - } - return ~position; - } - - @Override - public int scan(String periodStr, final int position) { - int sourceLength = periodStr.length(); - for (int pos = position; pos < sourceLength; pos++) { - for (String text : iSuffixesSortedDescByLength) { - if (periodStr.regionMatches(true, pos, text, 0, text.length()) && - !matchesOtherAffix(text.length(), periodStr, pos)) { - return pos; - } - } - } - return ~position; - } - - @Override - public String[] getAffixes() { - return iSuffixes.clone(); - } - } - - /** - * Builds a composite affix by merging two other affix implementations. - */ - private static class CompositeAffix extends IgnorableAffix { - private final PeriodFieldAffix iLeft; - private final PeriodFieldAffix iRight; - private final String[] iLeftRightCombinations; - - CompositeAffix(PeriodFieldAffix left, PeriodFieldAffix right) { - iLeft = left; - iRight = right; - - // We need to construct all possible combinations of left and right. - // We are doing it once in constructor so that getAffixes() is quicker. - Set result = new HashSet(); - for (String leftText : iLeft.getAffixes()) { - for (String rightText : iRight.getAffixes()) { - result.add(leftText + rightText); - } - } - iLeftRightCombinations = result.toArray(new String[result.size()]); - } - - @Override - public int calculatePrintedLength(int value) { - return iLeft.calculatePrintedLength(value) - + iRight.calculatePrintedLength(value); - } - - @Override - public void printTo(StringBuilder buf, int value) { - iLeft.printTo(buf, value); - iRight.printTo(buf, value); - } - - @Override - public void printTo(Writer out, int value) throws IOException { - iLeft.printTo(out, value); - iRight.printTo(out, value); - } - - @Override - public int parse(String periodStr, int position) { - int pos = iLeft.parse(periodStr, position); - if (pos >= 0) { - pos = iRight.parse(periodStr, pos); - if (pos >= 0 && matchesOtherAffix(parse(periodStr, pos) - pos, periodStr, position)) { - return ~position; - } - } - return pos; - } - - @Override - public int scan(String periodStr, final int position) { - int leftPosition = iLeft.scan(periodStr, position); - if (leftPosition >= 0) { - int rightPosition = iRight.scan(periodStr, iLeft.parse(periodStr, leftPosition)); - if (!(rightPosition >= 0 && matchesOtherAffix(iRight.parse(periodStr, rightPosition) - - leftPosition, periodStr, position))) { - if (leftPosition > 0) { - return leftPosition; - } else { - return rightPosition; - } - } - } - return ~position; - } - - @Override - public String[] getAffixes() { - return iLeftRightCombinations.clone(); - } - } - - /** - * Formats the numeric value of a field, potentially with prefix/suffix. - */ - private static class FieldFormatter implements PeriodPrinter, PeriodParser { - private final int iMinPrintedDigits; - //private final int iPrintZeroSetting; - private final int iMaxParsedDigits; - private final boolean iRejectSignedValues; - - /** - * The index of the field type, 0=year, etc. - */ - private final ChronoUnit unit; - /** - * The array of the latest formatter added for each type. - * This is shared between all the field formatters in a formatter. - */ - //private final FieldFormatter[] iFieldFormatters; - - private final PeriodFieldAffix iPrefix; - private final PeriodFieldAffix iSuffix; - - FieldFormatter(int minPrintedDigits, - int maxParsedDigits, boolean rejectSignedValues, - ChronoUnit chronoUnit, - PeriodFieldAffix prefix, PeriodFieldAffix suffix) { - iMinPrintedDigits = minPrintedDigits; - iMaxParsedDigits = maxParsedDigits; - iRejectSignedValues = rejectSignedValues; - this.unit = chronoUnit; - iPrefix = prefix; - iSuffix = suffix; - } - - FieldFormatter(FieldFormatter field, PeriodFieldAffix suffix) { - iMinPrintedDigits = field.iMinPrintedDigits; - iMaxParsedDigits = field.iMaxParsedDigits; - iRejectSignedValues = field.iRejectSignedValues; - this.unit = field.unit; - iPrefix = field.iPrefix; - if (field.iSuffix != null) { - suffix = new CompositeAffix(field.iSuffix, suffix); - } - iSuffix = suffix; - } - - public void finish(FieldFormatter[] fieldFormatters) { - Set prefixesToIgnore = new HashSet<>(); - Set suffixesToIgnore = new HashSet<>(); - for (FieldFormatter fieldFormatter : fieldFormatters) { - if (fieldFormatter != null && !this.equals(fieldFormatter)) { - prefixesToIgnore.add(fieldFormatter.iPrefix); - suffixesToIgnore.add(fieldFormatter.iSuffix); - } - } - // if we have a prefix then allow ignore behaviour - if (iPrefix != null) { - iPrefix.finish(prefixesToIgnore); - } - // if we have a suffix then allow ignore behaviour - if (iSuffix != null) { - iSuffix.finish(suffixesToIgnore); - } - } - - public int countFieldsToPrint(Period period, int stopAt, Locale locale) { - if (stopAt <= 0) { - return 0; - } - if (getFieldValue(period) != Long.MAX_VALUE) { - return 1; - } - return 0; - } - - public int calculatePrintedLength(Period period, Locale locale) { - long valueLong = getFieldValue(period); - if (valueLong == Long.MAX_VALUE) { - return 0; - } - - int sum = Math.max(FormatUtils.calculateDigitCount(valueLong), iMinPrintedDigits); - int value = (int) valueLong; - - if (iPrefix != null) { - sum += iPrefix.calculatePrintedLength(value); - } - if (iSuffix != null) { - sum += iSuffix.calculatePrintedLength(value); - } - - return sum; - } - - public void printTo(StringBuilder buf, Period period, Locale locale) { - long valueLong = getFieldValue(period); - if (valueLong == Long.MAX_VALUE) { - return; - } - int value = (int) valueLong; - if (iPrefix != null) { - iPrefix.printTo(buf, value); - } - int minDigits = iMinPrintedDigits; - if (minDigits <= 1) { - FormatUtils.appendUnpaddedInteger(buf, value); - } else { - FormatUtils.appendPaddedInteger(buf, value, minDigits); - } - if (iSuffix != null) { - iSuffix.printTo(buf, value); - } - } - - public void printTo(Writer out, Period period, Locale locale) throws IOException { - long valueLong = getFieldValue(period); - if (valueLong == Long.MAX_VALUE) { - return; - } - int value = (int) valueLong; - if (iPrefix != null) { - iPrefix.printTo(out, value); - } - int minDigits = iMinPrintedDigits; - if (minDigits <= 1) { - FormatUtils.writeUnpaddedInteger(out, value); - } else { - FormatUtils.writePaddedInteger(out, value, minDigits); - } - if (iSuffix != null) { - iSuffix.printTo(out, value); - } - } - - public int parseInto(PeriodAmount period, String text, int position, Locale locale) { - - if (position >= text.length()) { - return ~position; - } - - if (iPrefix != null) { - position = iPrefix.parse(text, position); - if (position < 0) { - return position; - } - } - int suffixPos = -1; - int limit; - limit = Math.min(iMaxParsedDigits, text.length() - position); - int length = 0; - int fractPos = -1; - boolean hasDigits = false; - boolean negative = false; - while (length < limit) { - char c = text.charAt(position + length); - // leading sign - if (length == 0 && (c == '-' || c == '+') && !iRejectSignedValues) { - negative = c == '-'; - - // Next character must be a digit. - if (length + 1 >= limit || - (c = text.charAt(position + length + 1)) < '0' || c > '9') { - break; - } - - if (negative) { - length++; - } else { - // Skip the '+' for parseInt to succeed. - position++; - } - // Expand the limit to disregard the sign character. - limit = Math.min(limit + 1, text.length() - position); - continue; - } - // main number - if (c >= '0' && c <= '9') { - hasDigits = true; - } else { - break; - } - length++; - } - - if (!hasDigits) { - return ~position; - } - setFieldValue(period, unit, parseInt(text, position, length)); - position += length; - if (position >= 0 && iSuffix != null) { - position = iSuffix.parse(text, position); - } - return position; - } - - /** - * @param text text to parse - * @param position position in text - * @param length exact count of characters to parse - * @return parsed int value - */ - private int parseInt(String text, int position, int length) { - if (length >= 10) { - return Integer.parseInt(text.substring(position, position + length)); - } - if (length <= 0) { - return 0; - } - int value = text.charAt(position++); - length--; - boolean negative; - if (value == '-') { - if (--length < 0) { - return 0; - } - negative = true; - value = text.charAt(position++); - } else { - negative = false; - } - value -= '0'; - while (length-- > 0) { - value = ((value << 3) + (value << 1)) + text.charAt(position++) - '0'; - } - return negative ? -value : value; - } - - /** - * @return Long.MAX_VALUE if nothing to print, otherwise value - */ - long getFieldValue(Period period) { - long value; - switch (unit) { - default: - return Long.MAX_VALUE; - case YEARS: - value = period.get(ChronoUnit.YEARS); - break; - case MONTHS: - value = period.get(ChronoUnit.MONTHS); - break; - case WEEKS: - value = period.get(ChronoUnit.WEEKS); - break; - case DAYS: - value = period.get(ChronoUnit.DAYS); - break; - case HOURS: - value = period.get(ChronoUnit.HOURS); - break; - case MINUTES: - value = period.get(ChronoUnit.MINUTES); - break; - case SECONDS: - value = period.get(ChronoUnit.SECONDS); - break; - case MILLIS: - value = period.get(ChronoUnit.MILLIS); - break; - } - - return value; - } - - void setFieldValue(PeriodAmount period, ChronoUnit field, long value) { - switch (field) { - default: - break; - case YEARS: - period.set(ChronoUnit.YEARS, value); - break; - case MONTHS: - period.set(ChronoUnit.MONTHS, value); - break; - case WEEKS: - period.set(ChronoUnit.WEEKS, value); - break; - case DAYS: - period.set(ChronoUnit.DAYS, value); - break; - case HOURS: - period.set(ChronoUnit.HOURS, value); - break; - case MINUTES: - period.set(ChronoUnit.MINUTES, value); - break; - case SECONDS: - period.set(ChronoUnit.SECONDS, value); - break; - case MILLIS: - period.set(ChronoUnit.MILLIS, value); - break; - } - } - - ChronoUnit getFieldType() { - return unit; - } - } - - /** - * Handles a simple literal piece of text. - */ - private static class Literal implements PeriodPrinter, PeriodParser { - static final Literal EMPTY = new Literal(""); - private final String iText; - - Literal(String text) { - iText = text; - } - - public int countFieldsToPrint(Period period, int stopAt, Locale locale) { - return 0; - } - - public int calculatePrintedLength(Period period, Locale locale) { - return iText.length(); - } - - public void printTo(StringBuilder buf, Period period, Locale locale) { - buf.append(iText); - } - - public void printTo(Writer out, Period period, Locale locale) throws IOException { - out.write(iText); - } - - public int parseInto(PeriodAmount period, String periodStr, - int position, Locale locale) { - if (periodStr.regionMatches(true, position, iText, 0, iText.length())) { - return position + iText.length(); - } - return ~position; - } - } - - /** - * Handles a separator, that splits the fields into multiple parts. - * For example, the 'T' in the ISO8601 standard. - */ - private static class Separator implements PeriodPrinter, PeriodParser { - private final String iText; - private final String iFinalText; - private final String[] iParsedForms; - - private final boolean iUseBefore; - private final boolean iUseAfter; - - private final PeriodPrinter iBeforePrinter; - private final PeriodParser iBeforeParser; - private volatile PeriodPrinter iAfterPrinter; - private volatile PeriodParser iAfterParser; - - Separator(String text, String finalText, String[] variants, - PeriodPrinter beforePrinter, PeriodParser beforeParser, - boolean useBefore, boolean useAfter) { - iText = text; - iFinalText = finalText; - - if ((finalText == null || text.equals(finalText)) && - (variants == null || variants.length == 0)) { - - iParsedForms = new String[]{text}; - } else { - // Filter and reverse sort the parsed forms. - TreeSet parsedSet = new TreeSet(String.CASE_INSENSITIVE_ORDER); - parsedSet.add(text); - parsedSet.add(finalText); - if (variants != null) { - for (int i = variants.length; --i >= 0; ) { - parsedSet.add(variants[i]); - } - } - ArrayList parsedList = new ArrayList(parsedSet); - Collections.reverse(parsedList); - iParsedForms = parsedList.toArray(new String[parsedList.size()]); - } - - iBeforePrinter = beforePrinter; - iBeforeParser = beforeParser; - iUseBefore = useBefore; - iUseAfter = useAfter; - } - - public int countFieldsToPrint(Period period, int stopAt, Locale locale) { - int sum = iBeforePrinter.countFieldsToPrint(period, stopAt, locale); - if (sum < stopAt) { - sum += iAfterPrinter.countFieldsToPrint(period, stopAt, locale); - } - return sum; - } - - public int calculatePrintedLength(Period period, Locale locale) { - PeriodPrinter before = iBeforePrinter; - PeriodPrinter after = iAfterPrinter; - - int sum = before.calculatePrintedLength(period, locale) - + after.calculatePrintedLength(period, locale); - - if (iUseBefore) { - if (before.countFieldsToPrint(period, 1, locale) > 0) { - if (iUseAfter) { - int afterCount = after.countFieldsToPrint(period, 2, locale); - if (afterCount > 0) { - sum += (afterCount > 1 ? iText : iFinalText).length(); - } - } else { - sum += iText.length(); - } - } - } else if (iUseAfter && after.countFieldsToPrint(period, 1, locale) > 0) { - sum += iText.length(); - } - - return sum; - } - - public void printTo(StringBuilder buf, Period period, Locale locale) { - PeriodPrinter before = iBeforePrinter; - PeriodPrinter after = iAfterPrinter; - - before.printTo(buf, period, locale); - if (iUseBefore) { - if (before.countFieldsToPrint(period, 1, locale) > 0) { - if (iUseAfter) { - int afterCount = after.countFieldsToPrint(period, 2, locale); - if (afterCount > 0) { - buf.append(afterCount > 1 ? iText : iFinalText); - } - } else { - buf.append(iText); - } - } - } else if (iUseAfter && after.countFieldsToPrint(period, 1, locale) > 0) { - buf.append(iText); - } - after.printTo(buf, period, locale); - } - - public void printTo(Writer out, Period period, Locale locale) throws IOException { - PeriodPrinter before = iBeforePrinter; - PeriodPrinter after = iAfterPrinter; - - before.printTo(out, period, locale); - if (iUseBefore) { - if (before.countFieldsToPrint(period, 1, locale) > 0) { - if (iUseAfter) { - int afterCount = after.countFieldsToPrint(period, 2, locale); - if (afterCount > 0) { - out.write(afterCount > 1 ? iText : iFinalText); - } - } else { - out.write(iText); - } - } - } else if (iUseAfter && after.countFieldsToPrint(period, 1, locale) > 0) { - out.write(iText); - } - after.printTo(out, period, locale); - } - - public int parseInto(PeriodAmount period, String periodStr, - int position, Locale locale) { - int oldPos = position; - position = iBeforeParser.parseInto(period, periodStr, position, locale); - - if (position < 0) { - return position; - } - - boolean found = false; - int parsedFormLength = -1; - if (position > oldPos) { - // Consume this separator. - for (String parsedForm : iParsedForms) { - if ((parsedForm == null || parsedForm.length() == 0) || - periodStr.regionMatches(true, position, parsedForm, 0, parsedForm.length())) { - parsedFormLength = (parsedForm == null ? 0 : parsedForm.length()); - position += parsedFormLength; - found = true; - break; - } - } - } - oldPos = position; - position = iAfterParser.parseInto(period, periodStr, position, locale); - if (position < 0) { - return position; - } - if (found && position == oldPos && parsedFormLength > 0) { - // Separator should not have been supplied. - return ~oldPos; - } - - if (position > oldPos && !found && !iUseBefore) { - // Separator was required. - return ~oldPos; - } - - return position; - } - - Separator finish(PeriodPrinter afterPrinter, PeriodParser afterParser) { - iAfterPrinter = afterPrinter; - iAfterParser = afterParser; - return this; - } - } - - /** - * Composite implementation that merges other fields to create a full pattern. - */ - private static class Composite implements PeriodPrinter, PeriodParser { - - private final PeriodPrinter[] iPrinters; - private final PeriodParser[] iParsers; - - Composite(List elementPairs) { - List printerList = new ArrayList<>(); - List parserList = new ArrayList<>(); - - decompose(elementPairs, printerList, parserList); - - if (printerList.size() <= 0) { - iPrinters = null; - } else { - iPrinters = printerList.toArray(new PeriodPrinter[printerList.size()]); - } - - if (parserList.size() <= 0) { - iParsers = null; - } else { - iParsers = parserList.toArray(new PeriodParser[parserList.size()]); - } - } - - public int countFieldsToPrint(Period period, int stopAt, Locale locale) { - int sum = 0; - PeriodPrinter[] printers = iPrinters; - for (int i = printers.length; sum < stopAt && --i >= 0; ) { - sum += printers[i].countFieldsToPrint(period, Integer.MAX_VALUE, locale); - } - return sum; - } - - public int calculatePrintedLength(Period period, Locale locale) { - int sum = 0; - PeriodPrinter[] printers = iPrinters; - for (int i = printers.length; --i >= 0; ) { - sum += printers[i].calculatePrintedLength(period, locale); - } - return sum; - } - - public void printTo(StringBuilder buf, Period period, Locale locale) { - PeriodPrinter[] printers = iPrinters; - int len = printers.length; - for (PeriodPrinter printer : printers) { - printer.printTo(buf, period, locale); - } - } - - public void printTo(Writer out, Period period, Locale locale) throws IOException { - PeriodPrinter[] printers = iPrinters; - int len = printers.length; - for (PeriodPrinter printer : printers) { - printer.printTo(out, period, locale); - } - } - - public int parseInto(PeriodAmount period, String periodStr, int position, Locale locale) { - PeriodParser[] parsers = iParsers; - if (parsers == null) { - throw new UnsupportedOperationException(); - } - - int len = parsers.length; - for (int i = 0; i < len && position >= 0; i++) { - position = parsers[i].parseInto(period, periodStr, position, locale); - } - return position; - } - - private void decompose(List elementPairs, List printerList, List parserList) { - int size = elementPairs.size(); - for (int i = 0; i < size; i += 2) { - Object element = elementPairs.get(i); - if (element instanceof PeriodPrinter) { - if (element instanceof Composite) { - addArrayToList(printerList, ((Composite) element).iPrinters); - } else { - printerList.add(element); - } - } - - element = elementPairs.get(i + 1); - if (element instanceof PeriodParser) { - if (element instanceof Composite) { - addArrayToList(parserList, ((Composite) element).iParsers); - } else { - parserList.add(element); - } - } - } - } - - private void addArrayToList(List list, Object[] array) { - if (array != null) { - Collections.addAll(list, array); - } - } - } } diff --git a/src/main/java/org/xbib/time/format/PluralAffix.java b/src/main/java/org/xbib/time/format/PluralAffix.java new file mode 100644 index 0000000..7d598f1 --- /dev/null +++ b/src/main/java/org/xbib/time/format/PluralAffix.java @@ -0,0 +1,82 @@ +package org.xbib.time.format; + +import java.io.IOException; +import java.io.Writer; + +/** + * Implements an affix where the text varies by the amount of the field. + * Only singular (1) and plural (not 1) are supported. + */ +class PluralAffix extends IgnorableAffix { + private final String iSingularText; + private final String iPluralText; + + PluralAffix(String singularText, String pluralText) { + iSingularText = singularText; + iPluralText = pluralText; + } + + public int calculatePrintedLength(int value) { + return (value == 1 ? iSingularText : iPluralText).length(); + } + + public void printTo(StringBuilder buf, int value) { + buf.append(value == 1 ? iSingularText : iPluralText); + } + + public void printTo(Writer out, int value) throws IOException { + out.write(value == 1 ? iSingularText : iPluralText); + } + + public int parse(String periodStr, int position) { + String text1 = iPluralText; + String text2 = iSingularText; + if (text1.length() < text2.length()) { + // Swap in order to match longer one first. + String temp = text1; + text1 = text2; + text2 = temp; + } + if (periodStr.regionMatches(true, position, text1, 0, text1.length()) && + !matchesOtherAffix(text1.length(), periodStr, position)) { + return position + text1.length(); + } + if (periodStr.regionMatches(true, position, text2, 0, text2.length()) && + !matchesOtherAffix(text2.length(), periodStr, position)) { + return position + text2.length(); + } + return ~position; + } + + public int scan(String periodStr, final int position) { + String text1 = iPluralText; + String text2 = iSingularText; + + if (text1.length() < text2.length()) { + // Swap in order to match longer one first. + String temp = text1; + text1 = text2; + text2 = temp; + } + + int textLength1 = text1.length(); + int textLength2 = text2.length(); + + int sourceLength = periodStr.length(); + for (int pos = position; pos < sourceLength; pos++) { + if (periodStr.regionMatches(true, pos, text1, 0, textLength1) && + !matchesOtherAffix(text1.length(), periodStr, pos)) { + return pos; + } + if (periodStr.regionMatches(true, pos, text2, 0, textLength2) && + !matchesOtherAffix(text2.length(), periodStr, pos)) { + return pos; + } + } + return ~position; + } + + public String[] getAffixes() { + return new String[]{iSingularText, iPluralText}; + } +} diff --git a/src/main/java/org/xbib/time/format/RegExAffix.java b/src/main/java/org/xbib/time/format/RegExAffix.java new file mode 100644 index 0000000..c3782c3 --- /dev/null +++ b/src/main/java/org/xbib/time/format/RegExAffix.java @@ -0,0 +1,97 @@ +package org.xbib.time.format; + +import java.io.IOException; +import java.io.Writer; +import java.util.Arrays; +import java.util.Comparator; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.regex.Pattern; + +/** + * Implements an affix where the text varies by the amount of the field. + * Different amounts are supported based on the provided parameters. + */ +class RegExAffix extends IgnorableAffix { + + private static final ConcurrentMap PATTERNS = new ConcurrentHashMap<>(); + + private static final Comparator LENGTH_DESC_COMPARATOR = (o1, o2) -> o2.length() - o1.length(); + + private final String[] iSuffixes; + private final Pattern[] iPatterns; + + // The parse method has to iterate over the suffixes from the longest one to the shortest one + // Otherwise it might consume not enough characters. + private final String[] iSuffixesSortedDescByLength; + + RegExAffix(String[] regExes, String[] texts) { + iSuffixes = texts.clone(); + iPatterns = new Pattern[regExes.length]; + for (int i = 0; i < regExes.length; i++) { + Pattern pattern = PATTERNS.get(regExes[i]); + if (pattern == null) { + pattern = Pattern.compile(regExes[i]); + Pattern p = PATTERNS.putIfAbsent(regExes[i], pattern); + } + iPatterns[i] = pattern; + } + iSuffixesSortedDescByLength = iSuffixes.clone(); + Arrays.sort(iSuffixesSortedDescByLength, LENGTH_DESC_COMPARATOR); + } + + private int selectSuffixIndex(int value) { + String valueString = String.valueOf(value); + for (int i = 0; i < iPatterns.length; i++) { + if (iPatterns[i].matcher(valueString).matches()) { + return i; + } + } + return iPatterns.length - 1; + } + + @Override + public int calculatePrintedLength(int value) { + return iSuffixes[selectSuffixIndex(value)].length(); + } + + @Override + public void printTo(StringBuilder buf, int value) { + buf.append(iSuffixes[selectSuffixIndex(value)]); + } + + @Override + public void printTo(Writer out, int value) throws IOException { + out.write(iSuffixes[selectSuffixIndex(value)]); + } + + @Override + public int parse(String periodStr, int position) { + for (String text : iSuffixesSortedDescByLength) { + if (periodStr.regionMatches(true, position, text, 0, text.length()) && + !matchesOtherAffix(text.length(), periodStr, position)) { + return position + text.length(); + } + } + return ~position; + } + + @Override + public int scan(String periodStr, final int position) { + int sourceLength = periodStr.length(); + for (int pos = position; pos < sourceLength; pos++) { + for (String text : iSuffixesSortedDescByLength) { + if (periodStr.regionMatches(true, pos, text, 0, text.length()) && + !matchesOtherAffix(text.length(), periodStr, pos)) { + return pos; + } + } + } + return ~position; + } + + @Override + public String[] getAffixes() { + return iSuffixes.clone(); + } +} diff --git a/src/main/java/org/xbib/time/format/Separator.java b/src/main/java/org/xbib/time/format/Separator.java new file mode 100644 index 0000000..3b4bf15 --- /dev/null +++ b/src/main/java/org/xbib/time/format/Separator.java @@ -0,0 +1,180 @@ +package org.xbib.time.format; + +import java.io.IOException; +import java.io.Writer; +import java.time.Period; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Locale; +import java.util.TreeSet; + +/** + * Handles a separator, that splits the fields into multiple parts. + * For example, the 'T' in the ISO8601 standard. + */ +class Separator implements PeriodPrinter, PeriodParser { + private final String iText; + private final String iFinalText; + private final String[] iParsedForms; + + private final boolean iUseBefore; + private final boolean iUseAfter; + + private final PeriodPrinter iBeforePrinter; + private final PeriodParser iBeforeParser; + volatile PeriodPrinter iAfterPrinter; + volatile PeriodParser iAfterParser; + + Separator(String text, String finalText, String[] variants, + PeriodPrinter beforePrinter, PeriodParser beforeParser, + boolean useBefore, boolean useAfter) { + iText = text; + iFinalText = finalText; + + if ((finalText == null || text.equals(finalText)) && + (variants == null || variants.length == 0)) { + + iParsedForms = new String[]{text}; + } else { + // Filter and reverse sort the parsed forms. + TreeSet parsedSet = new TreeSet(String.CASE_INSENSITIVE_ORDER); + parsedSet.add(text); + parsedSet.add(finalText); + if (variants != null) { + for (int i = variants.length; --i >= 0; ) { + parsedSet.add(variants[i]); + } + } + ArrayList parsedList = new ArrayList(parsedSet); + Collections.reverse(parsedList); + iParsedForms = parsedList.toArray(new String[parsedList.size()]); + } + + iBeforePrinter = beforePrinter; + iBeforeParser = beforeParser; + iUseBefore = useBefore; + iUseAfter = useAfter; + } + + public int countFieldsToPrint(Period period, int stopAt, Locale locale) { + int sum = iBeforePrinter.countFieldsToPrint(period, stopAt, locale); + if (sum < stopAt) { + sum += iAfterPrinter.countFieldsToPrint(period, stopAt, locale); + } + return sum; + } + + public int calculatePrintedLength(Period period, Locale locale) { + PeriodPrinter before = iBeforePrinter; + PeriodPrinter after = iAfterPrinter; + + int sum = before.calculatePrintedLength(period, locale) + + after.calculatePrintedLength(period, locale); + + if (iUseBefore) { + if (before.countFieldsToPrint(period, 1, locale) > 0) { + if (iUseAfter) { + int afterCount = after.countFieldsToPrint(period, 2, locale); + if (afterCount > 0) { + sum += (afterCount > 1 ? iText : iFinalText).length(); + } + } else { + sum += iText.length(); + } + } + } else if (iUseAfter && after.countFieldsToPrint(period, 1, locale) > 0) { + sum += iText.length(); + } + + return sum; + } + + public void printTo(StringBuilder buf, Period period, Locale locale) { + PeriodPrinter before = iBeforePrinter; + PeriodPrinter after = iAfterPrinter; + + before.printTo(buf, period, locale); + if (iUseBefore) { + if (before.countFieldsToPrint(period, 1, locale) > 0) { + if (iUseAfter) { + int afterCount = after.countFieldsToPrint(period, 2, locale); + if (afterCount > 0) { + buf.append(afterCount > 1 ? iText : iFinalText); + } + } else { + buf.append(iText); + } + } + } else if (iUseAfter && after.countFieldsToPrint(period, 1, locale) > 0) { + buf.append(iText); + } + after.printTo(buf, period, locale); + } + + public void printTo(Writer out, Period period, Locale locale) throws IOException { + PeriodPrinter before = iBeforePrinter; + PeriodPrinter after = iAfterPrinter; + + before.printTo(out, period, locale); + if (iUseBefore) { + if (before.countFieldsToPrint(period, 1, locale) > 0) { + if (iUseAfter) { + int afterCount = after.countFieldsToPrint(period, 2, locale); + if (afterCount > 0) { + out.write(afterCount > 1 ? iText : iFinalText); + } + } else { + out.write(iText); + } + } + } else if (iUseAfter && after.countFieldsToPrint(period, 1, locale) > 0) { + out.write(iText); + } + after.printTo(out, period, locale); + } + + public int parseInto(PeriodAmount period, String periodStr, + int pos, Locale locale) { + int position = pos; + int oldPos = position; + position = iBeforeParser.parseInto(period, periodStr, position, locale); + if (position < 0) { + return position; + } + boolean found = false; + int parsedFormLength = -1; + if (position > oldPos) { + for (String parsedForm : iParsedForms) { + if ((parsedForm == null || parsedForm.length() == 0) || + periodStr.regionMatches(true, position, parsedForm, 0, parsedForm.length())) { + parsedFormLength = (parsedForm == null ? 0 : parsedForm.length()); + position += parsedFormLength; + found = true; + break; + } + } + } + oldPos = position; + position = iAfterParser.parseInto(period, periodStr, position, locale); + if (position < 0) { + return position; + } + if (found && position == oldPos && parsedFormLength > 0) { + // Separator should not have been supplied. + return ~oldPos; + } + + if (position > oldPos && !found && !iUseBefore) { + // Separator was required. + return ~oldPos; + } + + return position; + } + + Separator finish(PeriodPrinter afterPrinter, PeriodParser afterParser) { + iAfterPrinter = afterPrinter; + iAfterParser = afterParser; + return this; + } +} diff --git a/src/main/java/org/xbib/time/format/SimpleAffix.java b/src/main/java/org/xbib/time/format/SimpleAffix.java new file mode 100644 index 0000000..342dc32 --- /dev/null +++ b/src/main/java/org/xbib/time/format/SimpleAffix.java @@ -0,0 +1,75 @@ +package org.xbib.time.format; + +import java.io.IOException; +import java.io.Writer; + +/** + * Implements an affix where the text does not vary by the amount. + */ +class SimpleAffix extends IgnorableAffix { + private final String iText; + + SimpleAffix(String text) { + iText = text; + } + + public int calculatePrintedLength(int value) { + return iText.length(); + } + + public void printTo(StringBuilder buf, int value) { + buf.append(iText); + } + + public void printTo(Writer out, int value) throws IOException { + out.write(iText); + } + + public int parse(String periodStr, int position) { + String text = iText; + int textLength = text.length(); + if (periodStr.regionMatches(true, position, text, 0, textLength) && + !matchesOtherAffix(textLength, periodStr, position)) { + return position + textLength; + } + return ~position; + } + + public int scan(String periodStr, final int position) { + String text = iText; + int textLength = text.length(); + int sourceLength = periodStr.length(); + search: + for (int pos = position; pos < sourceLength; pos++) { + if (periodStr.regionMatches(true, pos, text, 0, textLength) && + !matchesOtherAffix(textLength, periodStr, pos)) { + return pos; + } + // Only allow number characters to be skipped in search of suffix. + switch (periodStr.charAt(pos)) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '.': + case ',': + case '+': + case '-': + break; + default: + break search; + } + } + return ~position; + } + + public String[] getAffixes() { + return new String[]{iText}; + } +} diff --git a/src/main/java/org/xbib/time/pretty/PrettyTime.java b/src/main/java/org/xbib/time/pretty/PrettyTime.java index 2af7ef6..e98bddd 100644 --- a/src/main/java/org/xbib/time/pretty/PrettyTime.java +++ b/src/main/java/org/xbib/time/pretty/PrettyTime.java @@ -112,10 +112,7 @@ public class PrettyTime { * @return time unit quantity */ public TimeUnitQuantity approximateDuration(LocalDateTime then) { - if (then == null) { - then = LocalDateTime.now(); - } - long difference = ChronoUnit.MILLIS.between(localDateTime, then); + long difference = ChronoUnit.MILLIS.between(localDateTime, then == null ? LocalDateTime.now() : then); return calculateDuration(difference); } @@ -163,11 +160,8 @@ public class PrettyTime { * between compared dates. */ public List calculatePreciseDuration(LocalDateTime then) { - if (then == null) { - then = LocalDateTime.now(); - } List result = new ArrayList<>(); - long difference = ChronoUnit.MILLIS.between(localDateTime, then); + long difference = ChronoUnit.MILLIS.between(localDateTime, then == null ? LocalDateTime.now() : then); TimeUnitQuantity timeUnitQuantity = calculateDuration(difference); result.add(timeUnitQuantity); while (timeUnitQuantity.getDelta() != 0L) { @@ -191,10 +185,7 @@ public class PrettyTime { * @return A formatted string representing {@code then} */ public String format(LocalDateTime then) { - if (then == null) { - then = LocalDateTime.now(); - } - return format(approximateDuration(then)); + return format(approximateDuration(then == null ? LocalDateTime.now() : then)); } /** diff --git a/src/main/java/org/xbib/time/pretty/i18n/Resources_fi.java b/src/main/java/org/xbib/time/pretty/i18n/Resources_fi.java index a65f4d3..24fadb5 100644 --- a/src/main/java/org/xbib/time/pretty/i18n/Resources_fi.java +++ b/src/main/java/org/xbib/time/pretty/i18n/Resources_fi.java @@ -103,7 +103,7 @@ public class Resources_fi extends ListResourceBundle implements TimeFormatProvid {"MillenniumPastSuffix", "sitten"}, {"MillenniumFutureSuffix", "päästä"}, }; - private volatile ConcurrentMap formatMap = new ConcurrentHashMap(); + private volatile ConcurrentMap formatMap = new ConcurrentHashMap<>(); public Resources_fi() { } @@ -229,7 +229,7 @@ public class Resources_fi extends ListResourceBundle implements TimeFormatProvid @Override public String decorate(TimeUnitQuantity timeUnitQuantity, String time) { - String result = ""; + String result; if (timeUnitQuantity.getUnit() instanceof Day && Math.abs(timeUnitQuantity.getQuantity()) == 1) { result = time; } else { diff --git a/src/test/java/org/xbib/time/chronic/ParserTest.java b/src/test/java/org/xbib/time/chronic/ParserTest.java index 1717bf8..102de01 100644 --- a/src/test/java/org/xbib/time/chronic/ParserTest.java +++ b/src/test/java/org/xbib/time/chronic/ParserTest.java @@ -10,7 +10,7 @@ import java.time.ZonedDateTime; public class ParserTest extends Assert { - private final static ZoneId ZONE_ID = ZoneId.of("GMT"); + private static final ZoneId ZONE_ID = ZoneId.of("GMT"); private ZonedDateTime time_2006_08_16_14_00_00 = construct(2006, 8, 16, 14, 0, 0); public static ZonedDateTime construct(int year, int month) {