001/*
002 * Copyright (C) 2014 Jörg Prante
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.xbib.elasticsearch.plugin.jdbc.util;
017
018import java.util.ArrayList;
019import java.util.Calendar;
020import java.util.Date;
021import java.util.GregorianCalendar;
022import java.util.TimeZone;
023
024/**
025 * <p>Duration formatting utilities and constants. The following table describes the tokens
026 * used in the pattern language for formatting. </p>
027 * <table border="1">
028 * <tr><th>character</th><th>duration element</th></tr>
029 * <tr><td>y</td><td>years</td></tr>
030 * <tr><td>M</td><td>months</td></tr>
031 * <tr><td>d</td><td>days</td></tr>
032 * <tr><td>H</td><td>hours</td></tr>
033 * <tr><td>m</td><td>minutes</td></tr>
034 * <tr><td>s</td><td>seconds</td></tr>
035 * <tr><td>S</td><td>milliseconds</td></tr>
036 * </table>
037 */
038public class DurationFormatUtil {
039
040    /**
041     * Number of milliseconds in a standard second.
042     */
043    public static final long MILLIS_PER_SECOND = 1000;
044    /**
045     * Number of milliseconds in a standard minute.
046     */
047    public static final long MILLIS_PER_MINUTE = 60 * MILLIS_PER_SECOND;
048    /**
049     * Number of milliseconds in a standard hour.
050     */
051    public static final long MILLIS_PER_HOUR = 60 * MILLIS_PER_MINUTE;
052    /**
053     * Number of milliseconds in a standard day.
054     */
055    public static final long MILLIS_PER_DAY = 24 * MILLIS_PER_HOUR;
056
057    /**
058     * <p>DurationFormatUtils instances should NOT be constructed in standard programming.</p>
059     * <p/>
060     * <p>This constructor is public to permit tools that require a JavaBean instance
061     * to operate.</p>
062     */
063    public DurationFormatUtil() {
064        super();
065    }
066
067    /**
068     * <p>Pattern used with <code>FastDateFormat</code> and <code>SimpleDateFormat</code>
069     * for the ISO8601 period format used in durations.</p>
070     *
071     * @see java.text.SimpleDateFormat
072     */
073    public static final String ISO_EXTENDED_FORMAT_PATTERN = "'P'yyyy'Y'M'M'd'DT'H'H'm'M's.S'S'";
074
075    /**
076     * <p>Formats the time gap as a string.</p>
077     * <p/>
078     * <p>The format used is ISO8601-like:
079     * <i>H</i>:<i>m</i>:<i>s</i>.<i>S</i>.</p>
080     *
081     * @param durationMillis the duration to format
082     * @return the time as a String
083     */
084    public static String formatDurationHMS(long durationMillis) {
085        return formatDuration(durationMillis, "H:mm:ss.SSS");
086    }
087
088    /**
089     * <p>Formats the time gap as a string.</p>
090     * <p/>
091     * <p>The format used is the ISO8601 period format.</p>
092     * <p/>
093     * <p>This method formats durations using the days and lower fields of the
094     * ISO format pattern, such as P7D6TH5M4.321S.</p>
095     *
096     * @param durationMillis the duration to format
097     * @return the time as a String
098     */
099    public static String formatDurationISO(long durationMillis) {
100        return formatDuration(durationMillis, ISO_EXTENDED_FORMAT_PATTERN);
101    }
102
103    /**
104     * <p>Formats the time gap as a string, using the specified format, and
105     * using the default timezone.</p>
106     * <p/>
107     * <p>This method formats durations using the days and lower fields of the
108     * format pattern. Months and larger are not used.</p>
109     *
110     * @param durationMillis the duration to format
111     * @param format         the way in which to format the duration
112     * @return the time as a String
113     */
114    public static String formatDuration(long durationMillis, String format) {
115
116        Token[] tokens = lexx(format);
117
118        int days = 0;
119        int hours = 0;
120        int minutes = 0;
121        int seconds = 0;
122        int milliseconds = 0;
123
124        if (Token.containsTokenWithValue(tokens, d)) {
125            days = (int) (durationMillis / MILLIS_PER_DAY);
126            durationMillis = durationMillis - (days * MILLIS_PER_DAY);
127        }
128        if (Token.containsTokenWithValue(tokens, H)) {
129            hours = (int) (durationMillis / MILLIS_PER_HOUR);
130            durationMillis = durationMillis - (hours * MILLIS_PER_HOUR);
131        }
132        if (Token.containsTokenWithValue(tokens, m)) {
133            minutes = (int) (durationMillis / MILLIS_PER_MINUTE);
134            durationMillis = durationMillis - (minutes * MILLIS_PER_MINUTE);
135        }
136        if (Token.containsTokenWithValue(tokens, s)) {
137            seconds = (int) (durationMillis / MILLIS_PER_SECOND);
138            durationMillis = durationMillis - (seconds * MILLIS_PER_SECOND);
139        }
140        if (Token.containsTokenWithValue(tokens, S)) {
141            milliseconds = (int) durationMillis;
142        }
143
144        return format(tokens, 0, 0, days, hours, minutes, seconds, milliseconds);
145    }
146
147    /**
148     * <p>Formats an elapsed time into a plurialization correct string.</p>
149     * <p/>
150     * <p>This method formats durations using the days and lower fields of the
151     * format pattern. Months and larger are not used.</p>
152     *
153     * @param durationMillis               the elapsed time to report in milliseconds
154     * @param suppressLeadingZeroElements  suppresses leading 0 elements
155     * @param suppressTrailingZeroElements suppresses trailing 0 elements
156     * @return the formatted text in days/hours/minutes/seconds
157     */
158    public static String formatDurationWords(
159            long durationMillis,
160            boolean suppressLeadingZeroElements,
161            boolean suppressTrailingZeroElements) {
162
163        // This method is generally replacable by the format method, but 
164        // there are a series of tweaks and special cases that require 
165        // trickery to replicate.
166        String duration = formatDuration(durationMillis, "d' days 'H' hours 'm' minutes 's' seconds'");
167        if (suppressLeadingZeroElements) {
168            // this is a temporary marker on the front. Like ^ in regexp.
169            duration = " " + duration;
170            String tmp = Strings.replaceOnce(duration, " 0 days", "");
171            if (tmp.length() != duration.length()) {
172                duration = tmp;
173                tmp = Strings.replaceOnce(duration, " 0 hours", "");
174                if (tmp.length() != duration.length()) {
175                    duration = tmp;
176                    tmp = Strings.replaceOnce(duration, " 0 minutes", "");
177                    duration = tmp;
178                    if (tmp.length() != duration.length()) {
179                        duration = Strings.replaceOnce(tmp, " 0 seconds", "");
180                    }
181                }
182            }
183            if (duration.length() != 0) {
184                // strip the space off again
185                duration = duration.substring(1);
186            }
187        }
188        if (suppressTrailingZeroElements) {
189            String tmp = Strings.replaceOnce(duration, " 0 seconds", "");
190            if (tmp.length() != duration.length()) {
191                duration = tmp;
192                tmp = Strings.replaceOnce(duration, " 0 minutes", "");
193                if (tmp.length() != duration.length()) {
194                    duration = tmp;
195                    tmp = Strings.replaceOnce(duration, " 0 hours", "");
196                    if (tmp.length() != duration.length()) {
197                        duration = Strings.replaceOnce(tmp, " 0 days", "");
198                    }
199                }
200            }
201        }
202        // handle plurals
203        duration = " " + duration;
204        duration = Strings.replaceOnce(duration, " 1 seconds", " 1 second");
205        duration = Strings.replaceOnce(duration, " 1 minutes", " 1 minute");
206        duration = Strings.replaceOnce(duration, " 1 hours", " 1 hour");
207        duration = Strings.replaceOnce(duration, " 1 days", " 1 day");
208        return duration.trim();
209    }
210
211    /**
212     * <p>Formats the time gap as a string.</p>
213     * <p/>
214     * <p>The format used is the ISO8601 period format.</p>
215     *
216     * @param startMillis the start of the duration to format
217     * @param endMillis   the end of the duration to format
218     * @return the time as a String
219     */
220    public static String formatPeriodISO(long startMillis, long endMillis) {
221        return formatPeriod(startMillis, endMillis, ISO_EXTENDED_FORMAT_PATTERN, TimeZone.getDefault());
222    }
223
224    /**
225     * <p>Formats the time gap as a string, using the specified format.
226     *
227     * @param startMillis the start of the duration
228     * @param endMillis   the end of the duration
229     * @param format      the way in which to format the duration
230     * @return the time as a String
231     */
232    public static String formatPeriod(long startMillis, long endMillis, String format) {
233        return formatPeriod(startMillis, endMillis, format, TimeZone.getDefault());
234    }
235
236    /**
237     * <p>Formats the time gap as a string, using the specified format, and
238     * the timezone may be specified. </p>
239     * <p/>
240     * <p>When calculating the difference between months/days, it chooses to
241     * calculate months first. So when working out the number of months and
242     * days between January 15th and March 10th, it choose 1 month and
243     * 23 days gained by choosing January->February = 1 month and then
244     * calculating days forwards, and not the 1 month and 26 days gained by
245     * choosing March -> February = 1 month and then calculating days
246     * backwards. </p>
247     * <p/>
248     * <p>For more control, the <a href="http://joda-time.sf.net/">Joda-Time</a>
249     * library is recommended.</p>
250     *
251     * @param startMillis the start of the duration
252     * @param endMillis   the end of the duration
253     * @param format      the way in which to format the duration
254     * @param timezone    the millis are defined in
255     * @return the time as a String
256     */
257    public static String formatPeriod(long startMillis, long endMillis, String format,
258                                      TimeZone timezone) {
259
260        // Used to optimise for differences under 28 days and 
261        // called formatDuration(millis, format); however this did not work 
262        // over leap years. 
263        // TODO: Compare performance to see if anything was lost by 
264        // losing this optimisation. 
265
266        Token[] tokens = lexx(format);
267
268        // timezones get funky around 0, so normalizing everything to GMT 
269        // stops the hours being off
270        Calendar start = Calendar.getInstance(timezone);
271        start.setTime(new Date(startMillis));
272        Calendar end = Calendar.getInstance(timezone);
273        end.setTime(new Date(endMillis));
274
275        // initial estimates
276        int milliseconds = end.get(Calendar.MILLISECOND) - start.get(Calendar.MILLISECOND);
277        int seconds = end.get(Calendar.SECOND) - start.get(Calendar.SECOND);
278        int minutes = end.get(Calendar.MINUTE) - start.get(Calendar.MINUTE);
279        int hours = end.get(Calendar.HOUR_OF_DAY) - start.get(Calendar.HOUR_OF_DAY);
280        int days = end.get(Calendar.DAY_OF_MONTH) - start.get(Calendar.DAY_OF_MONTH);
281        int months = end.get(Calendar.MONTH) - start.get(Calendar.MONTH);
282        int years = end.get(Calendar.YEAR) - start.get(Calendar.YEAR);
283
284        // each initial estimate is adjusted in case it is under 0
285        while (milliseconds < 0) {
286            milliseconds += 1000;
287            seconds -= 1;
288        }
289        while (seconds < 0) {
290            seconds += 60;
291            minutes -= 1;
292        }
293        while (minutes < 0) {
294            minutes += 60;
295            hours -= 1;
296        }
297        while (hours < 0) {
298            hours += 24;
299            days -= 1;
300        }
301
302        if (Token.containsTokenWithValue(tokens, M)) {
303            while (days < 0) {
304                days += start.getActualMaximum(Calendar.DAY_OF_MONTH);
305                months -= 1;
306                start.add(Calendar.MONTH, 1);
307            }
308
309            while (months < 0) {
310                months += 12;
311                years -= 1;
312            }
313
314            if (!Token.containsTokenWithValue(tokens, y) && years != 0) {
315                while (years != 0) {
316                    months += 12 * years;
317                    years = 0;
318                }
319            }
320        } else {
321            // there are no M's in the format string
322
323            if (!Token.containsTokenWithValue(tokens, y)) {
324                int target = end.get(Calendar.YEAR);
325                if (months < 0) {
326                    // target is end-year -1
327                    target -= 1;
328                }
329
330                while ((start.get(Calendar.YEAR) != target)) {
331                    days += start.getActualMaximum(Calendar.DAY_OF_YEAR) - start.get(Calendar.DAY_OF_YEAR);
332
333                    // Not sure I grok why this is needed, but the brutal tests show it is
334                    if (start instanceof GregorianCalendar) {
335                        if ((start.get(Calendar.MONTH) == Calendar.FEBRUARY) &&
336                                (start.get(Calendar.DAY_OF_MONTH) == 29)) {
337                            days += 1;
338                        }
339                    }
340
341                    start.add(Calendar.YEAR, 1);
342
343                    days += start.get(Calendar.DAY_OF_YEAR);
344                }
345
346                years = 0;
347            }
348
349            while (start.get(Calendar.MONTH) != end.get(Calendar.MONTH)) {
350                days += start.getActualMaximum(Calendar.DAY_OF_MONTH);
351                start.add(Calendar.MONTH, 1);
352            }
353
354            months = 0;
355
356            while (days < 0) {
357                days += start.getActualMaximum(Calendar.DAY_OF_MONTH);
358                months -= 1;
359                start.add(Calendar.MONTH, 1);
360            }
361
362        }
363
364        // The rest of this code adds in values that 
365        // aren't requested. This allows the user to ask for the 
366        // number of months and get the real count and not just 0->11.
367
368        if (!Token.containsTokenWithValue(tokens, d)) {
369            hours += 24 * days;
370            days = 0;
371        }
372        if (!Token.containsTokenWithValue(tokens, H)) {
373            minutes += 60 * hours;
374            hours = 0;
375        }
376        if (!Token.containsTokenWithValue(tokens, m)) {
377            seconds += 60 * minutes;
378            minutes = 0;
379        }
380        if (!Token.containsTokenWithValue(tokens, s)) {
381            milliseconds += 1000 * seconds;
382            seconds = 0;
383        }
384
385        return format(tokens, years, months, days, hours, minutes, seconds, milliseconds);
386    }
387
388    /**
389     * <p>The internal method to do the formatting.</p>
390     *
391     * @param tokens       the tokens
392     * @param years        the number of years
393     * @param months       the number of months
394     * @param days         the number of days
395     * @param hours        the number of hours
396     * @param minutes      the number of minutes
397     * @param seconds      the number of seconds
398     * @param milliseconds the number of millis
399     * @return the formatted string
400     */
401    static String format(Token[] tokens, int years, int months, int days, int hours, int minutes, int seconds,
402                         int milliseconds) {
403        StringBuilder buffer = new StringBuilder();
404        boolean lastOutputSeconds = false;
405        int sz = tokens.length;
406        for (Token token : tokens) {
407            Object value = token.getValue();
408            if (value instanceof StringBuilder) {
409                buffer.append(value.toString());
410            } else {
411                if (value == y) {
412                    buffer.append(Integer.toString(years));
413                    lastOutputSeconds = false;
414                } else if (value == M) {
415                    buffer.append(Integer
416                            .toString(months));
417                    lastOutputSeconds = false;
418                } else if (value == d) {
419                    buffer.append(Integer
420                            .toString(days));
421                    lastOutputSeconds = false;
422                } else if (value == H) {
423                    buffer.append(Integer
424                            .toString(hours));
425                    lastOutputSeconds = false;
426                } else if (value == m) {
427                    buffer.append(Integer
428                            .toString(minutes));
429                    lastOutputSeconds = false;
430                } else if (value == s) {
431                    buffer.append(Integer
432                            .toString(seconds));
433                    lastOutputSeconds = true;
434                } else if (value == S) {
435                    if (lastOutputSeconds) {
436                        milliseconds += 1000;
437                        String str = Integer.toString(milliseconds);
438                        buffer.append(str.substring(1));
439                    } else {
440                        buffer.append(Integer.toString(milliseconds));
441                    }
442                    lastOutputSeconds = false;
443                }
444            }
445        }
446        return buffer.toString();
447    }
448
449    static final Object y = "y";
450    static final Object M = "M";
451    static final Object d = "d";
452    static final Object H = "H";
453    static final Object m = "m";
454    static final Object s = "s";
455    static final Object S = "S";
456
457    /**
458     * Parses a classic date format string into Tokens
459     *
460     * @param format to parse
461     * @return array of Token[]
462     */
463    static Token[] lexx(String format) {
464        char[] array = format.toCharArray();
465        ArrayList<Token> list = new ArrayList<Token>(array.length);
466
467        boolean inLiteral = false;
468        StringBuilder buffer = null;
469        Token previous = null;
470        int sz = array.length;
471        for (char ch : array) {
472            if (inLiteral && ch != '\'') {
473                buffer.append(ch);
474                continue;
475            }
476            Object value = null;
477            switch (ch) {
478                case '\'':
479                    if (inLiteral) {
480                        buffer = null;
481                        inLiteral = false;
482                    } else {
483                        buffer = new StringBuilder();
484                        list.add(new Token(buffer));
485                        inLiteral = true;
486                    }
487                    break;
488                case 'y':
489                    value = y;
490                    break;
491                case 'M':
492                    value = M;
493                    break;
494                case 'd':
495                    value = d;
496                    break;
497                case 'H':
498                    value = H;
499                    break;
500                case 'm':
501                    value = m;
502                    break;
503                case 's':
504                    value = s;
505                    break;
506                case 'S':
507                    value = S;
508                    break;
509                default:
510                    if (buffer == null) {
511                        buffer = new StringBuilder();
512                        list.add(new Token(buffer));
513                    }
514                    buffer.append(ch);
515            }
516
517            if (value != null) {
518                if (previous != null && previous.getValue() == value) {
519                    previous.increment();
520                } else {
521                    Token token = new Token(value);
522                    list.add(token);
523                    previous = token;
524                }
525                buffer = null;
526            }
527        }
528        return list.toArray(new Token[list.size()]);
529    }
530
531    /**
532     * Element that is parsed from the format pattern.
533     */
534    static class Token {
535
536        /**
537         * Helper method to determine if a set of tokens contain a value
538         *
539         * @param tokens set to look in
540         * @param value  to look for
541         * @return boolean <code>true</code> if contained
542         */
543        static boolean containsTokenWithValue(Token[] tokens, Object value) {
544            int sz = tokens.length;
545            for (Token token : tokens) {
546                if (token.getValue() == value) {
547                    return true;
548                }
549            }
550            return false;
551        }
552
553        private Object value;
554        private int count;
555
556        /**
557         * Wraps a token around a value. A value would be something like a 'Y'.
558         *
559         * @param value to wrap
560         */
561        Token(Object value) {
562            this.value = value;
563            this.count = 1;
564        }
565
566        /**
567         * Wraps a token around a repeated number of a value, for example it would
568         * store 'yyyy' as a value for y and a count of 4.
569         *
570         * @param value to wrap
571         * @param count to wrap
572         */
573        Token(Object value, int count) {
574            this.value = value;
575            this.count = count;
576        }
577
578        /**
579         * Adds another one of the value
580         */
581        void increment() {
582            count++;
583        }
584
585        /**
586         * Gets the current number of values represented
587         *
588         * @return int number of values represented
589         */
590        int getCount() {
591            return count;
592        }
593
594        /**
595         * Gets the particular value this token represents.
596         *
597         * @return Object value
598         */
599        Object getValue() {
600            return value;
601        }
602
603        /**
604         * Supports equality of this Token to another Token.
605         *
606         * @param obj2 Object to consider equality of
607         * @return boolean <code>true</code> if equal
608         */
609        public boolean equals(Object obj2) {
610            if (obj2 instanceof Token) {
611                Token tok2 = (Token) obj2;
612                if (this.value.getClass() != tok2.value.getClass()) {
613                    return false;
614                }
615                if (this.count != tok2.count) {
616                    return false;
617                }
618                if (this.value instanceof StringBuilder) {
619                    return this.value.toString().equals(tok2.value.toString());
620                } else if (this.value instanceof Number) {
621                    return this.value.equals(tok2.value);
622                } else {
623                    return this.value == tok2.value;
624                }
625            }
626            return false;
627        }
628
629        /**
630         * Returns a hashcode for the token equal to the
631         * hashcode for the token's value. Thus 'TT' and 'TTTT'
632         * will have the same hashcode.
633         *
634         * @return The hashcode for the token
635         */
636        public int hashCode() {
637            return this.value.hashCode();
638        }
639
640    }
641
642}