001package co.codewizards.cloudstore.core;
002
003import static co.codewizards.cloudstore.core.util.StringUtil.*;
004
005import java.text.ParseException;
006import java.util.Arrays;
007import java.util.Collection;
008import java.util.HashMap;
009import java.util.HashSet;
010import java.util.LinkedHashMap;
011import java.util.Map;
012import java.util.Set;
013import java.util.StringTokenizer;
014
015public class TimePeriod {
016        private final long millis;
017
018        private static final String delim = " \t\u202F\u2009\r\n";
019        private static final Set<Character> delimChars = new HashSet<Character>(delim.length());
020        static {
021                for (char c : delim.toCharArray())
022                        delimChars.add(c);
023        }
024
025        public TimePeriod(String string) throws ParseException {
026                final Map<TimeUnit, Long> timeUnitMap = new HashMap<>();
027                final StringTokenizer st = new StringTokenizer(string, delim, true);
028                int offset = 0;
029                long value = Long.MIN_VALUE;
030                String timeUnitString = null;
031                while (st.hasMoreTokens()) {
032                        final String tok = st.nextToken();
033                        if (! isDelim(tok)) {
034                                if (value == Long.MIN_VALUE) {
035                                        // tok might be a combination of a number and a unit, if no delimiter is placed inbetween => try to split!
036                                        final String[] numberAndTimeUnit = splitNumberAndTimeUnit(tok);
037                                        try {
038                                                value = Long.parseLong(numberAndTimeUnit[0]);
039                                        } catch (NumberFormatException x) {
040                                                throw new ParseException(
041                                                                String.format("The text '%s' at position %d (0-based) of the input '%s' is not a valid integer!",
042                                                                                isEmpty(numberAndTimeUnit[0]) ? tok : numberAndTimeUnit[0], offset, string),
043                                                                offset);
044                                        }
045
046                                        timeUnitString = numberAndTimeUnit[1];
047                                }
048                                else
049                                        timeUnitString = tok;
050
051                                if (! isEmpty(timeUnitString)) {
052                                        final TimeUnit timeUnit;
053                                        try {
054                                                timeUnit = TimeUnit.valueOf(timeUnitString);
055                                        } catch (Exception x) {
056                                                throw new ParseException(
057                                                                String.format("The text '%s' at position %d (0-based) of the input '%s' is not a valid time unit!",
058                                                                                timeUnitString, offset, string),
059                                                                offset);
060                                        }
061                                        timeUnitMap.put(timeUnit, value);
062                                        value = Long.MIN_VALUE;
063                                        timeUnitString = null;
064                                }
065                        }
066                        offset += tok.length();
067                }
068
069                if (value != Long.MIN_VALUE)
070                        throw new ParseException(String.format("The input '%s' is missing a time unit at its end!", string), offset);
071
072                long millis = 0;
073                for (Map.Entry<TimeUnit, Long> me : timeUnitMap.entrySet())
074                        millis += me.getKey().toMillis(me.getValue());
075
076                this.millis = millis;
077        }
078
079        private static boolean isDelim(final String token) {
080                if (token == null || token.isEmpty())
081                        return true;
082
083                for (final char c : token.toCharArray()) {
084                        if (! delimChars.contains(c))
085                                return false;
086                }
087                return true;
088        }
089
090        private static String[] splitNumberAndTimeUnit(final String token) {
091                if (token == null || token.isEmpty())
092                        return new String[] { token, null };
093
094                int index = 0;
095                while (Character.isDigit(token.charAt(index))) {
096                        if (++index >= token.length())
097                                return new String[] { token, null };
098                }
099                return new String[] { token.substring(0, index), token.substring(index) };
100        }
101
102        public TimePeriod(long millis) {
103                this.millis = millis;
104        }
105
106        @Override
107        public String toString() {
108                return toString(TimeUnit.getUniqueTimeUnitsOrderedByLengthDesc());
109        }
110
111        public String toString(final TimeUnit ... timeUnits) {
112                return toString(timeUnits == null ? null : new HashSet<>(Arrays.asList(timeUnits)));
113        }
114
115        public String toString(final Collection<TimeUnit> timeUnits) {
116                final StringBuilder sb = new StringBuilder();
117                final Map<TimeUnit, Long> timeUnitMap = toTimeUnitMap(timeUnits);
118                for (Map.Entry<TimeUnit, Long> me : timeUnitMap.entrySet()) {
119                        if (sb.length() > 0)
120                                sb.append(' ');
121
122                        sb.append(me.getValue()).append('\u202F').append(me.getKey()); // thin-space-separated
123                }
124                return sb.toString();
125        }
126
127        /**
128         * Gets the value of this time period in milliseconds.
129         * @return the value of this time period in milliseconds.
130         */
131        public long toMillis() {
132                return millis;
133        }
134
135        public Map<TimeUnit, Long> toTimeUnitMap() {
136                return toTimeUnitMap((Collection<TimeUnit>) null);
137        }
138
139        public Map<TimeUnit, Long> toTimeUnitMap(final TimeUnit ... timeUnits) {
140                return toTimeUnitMap(timeUnits == null ? null : new HashSet<>(Arrays.asList(timeUnits)));
141        }
142
143        public Map<TimeUnit, Long> toTimeUnitMap(Collection<TimeUnit> timeUnits) {
144                if (timeUnits == null)
145                        timeUnits = TimeUnit.getUniqueTimeUnitsOrderedByLengthAsc();
146
147                final Map<TimeUnit, Long> result = new LinkedHashMap<>();
148                long remaining = millis;
149                for (final TimeUnit timeUnit : timeUnits) {
150                        final long v = remaining / timeUnit.toMillis();
151                        remaining -= v * timeUnit.toMillis();
152
153                        if (v != 0)
154                                result.put(timeUnit, v);
155                }
156
157                if (remaining != 0)
158                        result.put(TimeUnit.ms, remaining);
159
160                return result;
161        }
162
163        @Override
164        public int hashCode() {
165                final int prime = 31;
166                int result = 1;
167                result = prime * result + (int) (millis ^ (millis >>> 32));
168                return result;
169        }
170
171        @Override
172        public boolean equals(Object obj) {
173                if (this == obj) return true;
174                if (obj == null) return false;
175                if (getClass() != obj.getClass()) return false;
176
177                final TimePeriod other = (TimePeriod) obj;
178                return this.millis == other.millis;
179        }
180
181}