001/*
002 * Copyright (c) 2013 Nu Echo Inc. All rights reserved.
003 */
004
005package com.nuecho.rivr.core.util;
006
007import java.io.*;
008import java.util.regex.*;
009
010/**
011 * Represents a delta of time.
012 * 
013 * @author Nu Echo Inc.
014 */
015public final class Duration implements Comparable<Duration>, Serializable {
016
017    /* Even though they fit inside an integer, those constant should be kept as long to avoid 
018     * potential mistakes (overflow) when doing arithmetics with them. By having them in a long,
019     * we avoid forgetful people who might do WEEK_IN_MILLIS * 1000 instead of WEEK_IN_MILLIS * 1000L
020     */
021    public static final long SECOND_IN_MILLIS = 1000;
022    public static final long MINUTE_IN_MILLIS = 60 * SECOND_IN_MILLIS;
023    public static final long HOUR_IN_MILLIS = 60 * MINUTE_IN_MILLIS;
024    public static final long DAY_IN_MILLIS = 24 * HOUR_IN_MILLIS;
025    public static final long WEEK_IN_MILLIS = 7 * DAY_IN_MILLIS;
026    public static final long YEAR_IN_MILLIS = DAY_IN_MILLIS * 365L + HOUR_IN_MILLIS * 6;
027
028    private static final long serialVersionUID = 1L;
029
030    public static final Duration ZERO = new Duration(0);
031    public static final Pattern DURATION_EXPRESSION_REGEXP = Pattern.compile("((\\d+)\\s*y)?\\s*"
032                                                                             + "((\\d+)\\s*d)?\\s*"
033                                                                             + "((\\d+)\\s*h)?\\s*"
034                                                                             + "((\\d+)\\s*m)?\\s*"
035                                                                             + "((\\d+)\\s*s)?\\s*"
036                                                                             + "((\\d+)\\s*ms)?");
037
038    public static Duration seconds(long seconds) {
039        Assert.between(0, seconds, Long.MAX_VALUE / SECOND_IN_MILLIS);
040        return new Duration(seconds * SECOND_IN_MILLIS);
041    }
042
043    public static Duration minutes(long minutes) {
044        Assert.between(0, minutes, Long.MAX_VALUE / MINUTE_IN_MILLIS);
045        return new Duration(minutes * MINUTE_IN_MILLIS);
046    }
047
048    public static Duration hours(long hours) {
049        Assert.between(0, hours, Long.MAX_VALUE / HOUR_IN_MILLIS);
050        return new Duration(hours * HOUR_IN_MILLIS);
051    }
052
053    public static Duration days(long days) {
054        Assert.between(0, days, Long.MAX_VALUE / DAY_IN_MILLIS);
055        return new Duration(days * DAY_IN_MILLIS);
056    }
057
058    public static Duration year(long year) {
059        Assert.between(0, year, Long.MAX_VALUE / YEAR_IN_MILLIS);
060        return new Duration(year * YEAR_IN_MILLIS);
061    }
062
063    public static Duration milliseconds(long milliseconds) {
064        Assert.notNegative(milliseconds, "milliseconds");
065        return new Duration(milliseconds);
066    }
067
068    public static Duration parse(String text) {
069        Duration duration = Duration.ZERO;
070
071        text = text.trim();
072        Matcher matcher = DURATION_EXPRESSION_REGEXP.matcher(text);
073
074        if (!matcher.matches()) throw new IllegalArgumentException("Invalid time value syntax: [" + text + "]");
075
076        String yearsMatch = matcher.group(2);
077        if (yearsMatch != null) {
078            duration = Duration.sum(duration, Duration.year(Long.parseLong(yearsMatch)));
079        }
080
081        String daysMatch = matcher.group(4);
082        if (daysMatch != null) {
083            duration = Duration.sum(duration, Duration.days(Long.parseLong(daysMatch)));
084        }
085
086        String hoursMatch = matcher.group(6);
087        if (hoursMatch != null) {
088            duration = Duration.sum(duration, Duration.hours(Long.parseLong(hoursMatch)));
089        }
090
091        String minutesMatch = matcher.group(8);
092        if (minutesMatch != null) {
093            duration = Duration.sum(duration, Duration.minutes(Long.parseLong(minutesMatch)));
094        }
095
096        String secondsMatch = matcher.group(10);
097        if (secondsMatch != null) {
098            duration = Duration.sum(duration, Duration.seconds(Long.parseLong(secondsMatch)));
099        }
100
101        String millisecondsMatch = matcher.group(12);
102        if (millisecondsMatch != null) {
103            duration = Duration.sum(duration, Duration.milliseconds(Long.parseLong(millisecondsMatch)));
104        }
105        return duration;
106    }
107
108    public static Duration sum(Duration a, Duration b) {
109        long millisecondsA = a.getMilliseconds();
110        long millisecondsB = b.getMilliseconds();
111
112        long allowedMax = Long.MAX_VALUE - millisecondsA;
113        if (millisecondsB > allowedMax) throw new AssertionError("Time sum would overflow Long capacity.");
114
115        return new Duration(millisecondsA + millisecondsB);
116    }
117
118    private final long mMilliseconds;
119
120    private Duration(long milliseconds) {
121        mMilliseconds = milliseconds;
122    }
123
124    public long getMilliseconds() {
125        return mMilliseconds;
126    }
127
128    @Override
129    public int compareTo(Duration other) {
130        // Don't simply return (int) mMilliseconds - other.mMilliseconds because of potential int overflow.
131        long diff = mMilliseconds - other.mMilliseconds;
132        if (diff < 0) return -1;
133        else if (diff > 0) return 1;
134        return 0;
135    }
136
137    @Override
138    public String toString() {
139        if (mMilliseconds == 0) return "0 ms";
140
141        StringBuffer buffer = new StringBuffer();
142
143        long value = append(buffer, YEAR_IN_MILLIS, "year", mMilliseconds, true, true);
144        value = append(buffer, DAY_IN_MILLIS, "day", value, true, true);
145        value = append(buffer, HOUR_IN_MILLIS, "hour", value, true, true);
146        value = append(buffer, MINUTE_IN_MILLIS, "minute", value, true, true);
147        value = append(buffer, SECOND_IN_MILLIS, "second", value, true, true);
148        append(buffer, 1, "millisecond", value, true, true);
149
150        if (mMilliseconds > 1000) {
151            buffer.append("(");
152            buffer.append(mMilliseconds);
153            buffer.append(" ms)");
154        }
155        return buffer.toString();
156    }
157
158    private long append(StringBuffer buffer,
159                        long constant,
160                        String label,
161                        long baseValue,
162                        boolean appendPlural,
163                        boolean appendWhitespace) {
164        long value = baseValue / constant;
165        if (value == 0) return baseValue;
166        buffer.append(value);
167
168        if (appendWhitespace) {
169            buffer.append(" ");
170        }
171        buffer.append(label);
172        if (value > 1 && appendPlural) {
173            buffer.append("s");
174        }
175        if (appendWhitespace) {
176            buffer.append(" ");
177        }
178        return baseValue % constant;
179    }
180
181    @Override
182    public int hashCode() {
183        final int prime = 31;
184        int result = 1;
185        result = prime * result + (int) (mMilliseconds ^ mMilliseconds >>> 32);
186        return result;
187    }
188
189    @Override
190    public boolean equals(Object obj) {
191        if (this == obj) return true;
192        if (obj == null) return false;
193        if (getClass() != obj.getClass()) return false;
194        Duration other = (Duration) obj;
195        if (mMilliseconds != other.mMilliseconds) return false;
196        return true;
197    }
198
199}