com.eaio.util.text.HumanTime.java Source code

Java tutorial

Introduction

Here is the source code for com.eaio.util.text.HumanTime.java

Source

/*
 * HumanTime.java
 * 
 * Created on 06.10.2008
 * 
 * Copyright (c) 2008 Johann Burkard (<mailto:jb@eaio.com>) <http://eaio.com>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
 * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
 * Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
 * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 * 
 */
package com.eaio.util.text;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.Iterator;

/**
 * HumanTime parses and formats time deltas for easier reading by humans. It can format time information without losing
 * information but its main purpose is to generate more easily understood approximations.
 * <h3>Using HumanTime</h3>
 * <p>
 * Use HumanTime by creating an instance that contains the time delta ({@link HumanTime#HumanTime(long)}), create an
 * empty instance through ({@link HumanTime#HumanTime()}) and set the delta using the {@link #y()}, {@link #d()},
 * {@link #h()}, {@link #s()} and {@link #ms()} methods or parse a {@link CharSequence} representation ({@link #eval(CharSequence)}).
 * Parsing ignores whitespace and is case insensitive.
 * </p>
 * <h3>HumanTime format</h3>
 * <p>
 * HumanTime will format time deltas in years ("y"), days ("d"), hours ("h"), minutes ("m"), seconds ("s") and
 * milliseconds ("ms"), separated by a blank character. For approximate representations, the time delta will be round up
 * or down if necessary.
 * </p>
 * <h3>HumanTime examples</h3>
 * <ul>
 * <li>HumanTime.eval("1 d 1d 2m 3m").getExactly() = "2 d 5 m"</li>
 * <li>HumanTime.eval("2m8d2h4m").getExactly() = "8 d 2 h 6 m"</li>
 * <li>HumanTime.approximately("2 d 8 h 20 m 50 s") = "2 d 8 h"</li>
 * <li>HumanTime.approximately("55m") = "1 h"</li>
 * </ul>
 * <h3>Implementation details</h3>
 * <ul>
 * <li>The time delta can only be increased.</li>
 * <li>Instances of this class are thread safe.</li>
 * <li>Getters using the Java Beans naming conventions are provided for use in environments like JSP or with expression
 * languages like OGNL. See {@link #getApproximately()} and {@link #getExactly()}.</li>
 * <li>To keep things simple, a year consists of 365 days.</li>
 * </ul>
 * 
 * @author <a href="mailto:jb@eaio.com">Johann Burkard</a>
 * @version $Id: HumanTime.java 323 2008-10-08 19:06:22Z Johann $
 * @see #eval(CharSequence)
 * @see #approximately(CharSequence)
 * @see <a href="http://johannburkard.de/blog/programming/java/date-formatting-parsing-humans-humantime.html">Date Formatting and Parsing for Humans in Java with HumanTime</a>
 */
public class HumanTime implements Externalizable, Comparable<HumanTime>, Cloneable {

    private static Log logger = LogFactory.getLog(HumanTime.class);

    /**
     * The serial version UID.
     */
    private static final long serialVersionUID = 5179328390732826722L;

    /**
     * One second.
     */
    private static final long SECOND = 1000;

    /**
     * One minute.
     */
    private static final long MINUTE = SECOND * 60;

    /**
     * One hour.
     */
    private static final long HOUR = MINUTE * 60;

    /**
     * One day.
     */
    private static final long DAY = HOUR * 24;

    /**
     * One year.
     */
    private static final long YEAR = DAY * 365;

    /**
     * Percentage of what is round up or down.
     */
    private static final int CEILING_PERCENTAGE = 15;

    /**
     * Parsing state.
     */
    static enum State {

        NUMBER, IGNORED, UNIT

    }

    static State getState(char c) {
        State out;
        switch (c) {
        case '0':
        case '1':
        case '2':
        case '3':
        case '4':
        case '5':
        case '6':
        case '7':
        case '8':
        case '9':
            out = State.NUMBER;
            break;
        case 's':
        case 'm':
        case 'h':
        case 'd':
        case 'y':
        case 'S':
        case 'M':
        case 'H':
        case 'D':
        case 'Y':
            out = State.UNIT;
            break;
        default:
            out = State.IGNORED;
        }
        return out;
    }

    /**
     * Parses a {@link CharSequence} argument and returns a {@link HumanTime} instance.
     * 
     * @param s the char sequence, may not be <code>null</code>
     * @return an instance, never <code>null</code>
     */
    public static HumanTime eval(final CharSequence s) {
        HumanTime out = new HumanTime(0L);

        int num = 0;

        int start = 0;
        int end = 0;

        State oldState = State.IGNORED;

        for (char c : new Iterable<Character>() {

            /**
             * @see java.lang.Iterable#iterator()
             */
            public Iterator<Character> iterator() {
                return new Iterator<Character>() {

                    private int p = 0;

                    /**
                     * @see java.util.Iterator#hasNext()
                     */
                    public boolean hasNext() {
                        return p < s.length();
                    }

                    /**
                     * @see java.util.Iterator#next()
                     */
                    public Character next() {
                        return s.charAt(p++);
                    }

                    /**
                     * @see java.util.Iterator#remove()
                     */
                    public void remove() {
                        throw new UnsupportedOperationException();
                    }

                };
            }

        }) {
            State newState = getState(c);
            if (oldState != newState) {
                if (oldState == State.NUMBER && (newState == State.IGNORED || newState == State.UNIT)) {
                    num = Integer.parseInt(s.subSequence(start, end).toString());
                } else if (oldState == State.UNIT && (newState == State.IGNORED || newState == State.NUMBER)) {
                    out.nTimes(s.subSequence(start, end).toString(), num);
                    num = 0;
                }
                start = end;
            }
            ++end;
            oldState = newState;
        }
        if (oldState == State.UNIT) {
            out.nTimes(s.subSequence(start, end).toString(), num);
        }

        return out;
    }

    /**
     * Parses and formats the given char sequence, preserving all data.
     * <p>
     * Equivalent to <code>eval(in).getExactly()</code>
     * 
     * @param in the char sequence, may not be <code>null</code>
     * @return a formatted String, never <code>null</code>
     */
    public static String exactly(CharSequence in) {
        return eval(in).getExactly();
    }

    /**
     * Formats the given time delta, preserving all data.
     * <p>
     * Equivalent to <code>new HumanTime(in).getExactly()</code>
     * 
     * @param l the time delta
     * @return a formatted String, never <code>null</code>
     */
    public static String exactly(long l) {
        return new HumanTime(l).getExactly();
    }

    /**
     * Parses and formats the given char sequence, potentially removing some data to make the output easier to
     * understand.
     * <p>
     * Equivalent to <code>eval(in).getApproximately()</code>
     * 
     * @param in the char sequence, may not be <code>null</code>
     * @return a formatted String, never <code>null</code>
     */
    public static String approximately(CharSequence in) {
        return eval(in).getApproximately();
    }

    /**
     * Formats the given time delta, preserving all data.
     * <p>
     * Equivalent to <code>new HumanTime(l).getApproximately()</code>
     * 
     * @param l the time delta
     * @return a formatted String, never <code>null</code>
     */
    public static String approximately(long l) {
        return new HumanTime(l).getApproximately();
    }

    /**
     * The time delta.
     */
    private long delta;

    /**
     * No-argument Constructor for HumanTime.
     * <p>
     * Equivalent to calling <code>new HumanTime(0L)</code>.
     */
    public HumanTime() {
        this(0L);
    }

    /**
     * Constructor for HumanTime.
     * 
     * @param delta the initial time delta, interpreted as a positive number
     */
    public HumanTime(long delta) {
        super();
        this.delta = Math.abs(delta);
    }

    private void nTimes(String unit, int n) {
        if ("ms".equalsIgnoreCase(unit)) {
            ms(n);
        } else if ("s".equalsIgnoreCase(unit)) {
            s(n);
        } else if ("m".equalsIgnoreCase(unit)) {
            m(n);
        } else if ("h".equalsIgnoreCase(unit)) {
            h(n);
        } else if ("d".equalsIgnoreCase(unit)) {
            d(n);
        } else if ("y".equalsIgnoreCase(unit)) {
            y(n);
        }
    }

    private long upperCeiling(long x) {
        return (x / 100) * (100 - CEILING_PERCENTAGE);
    }

    private long lowerCeiling(long x) {
        return (x / 100) * CEILING_PERCENTAGE;
    }

    private String ceil(long d, long n) {
        return Integer.toString((int) Math.ceil((double) d / n));
    }

    private String floor(long d, long n) {
        return Integer.toString((int) Math.floor((double) d / n));
    }

    /**
     * Adds one year to the time delta.
     * 
     * @return this HumanTime object
     */
    public HumanTime y() {
        return y(1);
    }

    /**
     * Adds n years to the time delta.
     * 
     * @param n n
     * @return this HumanTime object
     */
    public HumanTime y(int n) {
        delta += YEAR * Math.abs(n);
        return this;
    }

    /**
     * Adds one day to the time delta.
     * 
     * @return this HumanTime object
     */
    public HumanTime d() {
        return d(1);
    }

    /**
     * Adds n days to the time delta.
     * 
     * @param n n
     * @return this HumanTime object
     */
    public HumanTime d(int n) {
        delta += DAY * Math.abs(n);
        return this;
    }

    /**
     * Adds one hour to the time delta.
     * 
     * @return this HumanTime object
     */
    public HumanTime h() {
        return h(1);
    }

    /**
     * Adds n hours to the time delta.
     * 
     * @param n n
     * @return this HumanTime object
     */
    public HumanTime h(int n) {
        delta += HOUR * Math.abs(n);
        return this;
    }

    /**
     * Adds one month to the time delta.
     * 
     * @return this HumanTime object
     */
    public HumanTime m() {
        return m(1);
    }

    /**
     * Adds n months to the time delta.
     * 
     * @param n n
     * @return this HumanTime object
     */
    public HumanTime m(int n) {
        delta += MINUTE * Math.abs(n);
        return this;
    }

    /**
     * Adds one second to the time delta.
     * 
     * @return this HumanTime object
     */
    public HumanTime s() {
        return s(1);
    }

    /**
     * Adds n seconds to the time delta.
     * 
     * @param n seconds
     * @return this HumanTime object
     */
    public HumanTime s(int n) {
        delta += SECOND * Math.abs(n);
        return this;
    }

    /**
     * Adds one millisecond to the time delta.
     * 
     * @return this HumanTime object
     */
    public HumanTime ms() {
        return ms(1);
    }

    /**
     * Adds n milliseconds to the time delta.
     * 
     * @param n n
     * @return this HumanTime object
     */
    public HumanTime ms(int n) {
        delta += Math.abs(n);
        return this;
    }

    /**
     * Returns a human-formatted representation of the time delta.
     * 
     * @return a formatted representation of the time delta, never <code>null</code>
     */
    public String getExactly() {
        return getExactly(new StringBuilder()).toString();
    }

    /**
     * Appends a human-formatted representation of the time delta to the given {@link Appendable} object.
     * 
     * @param <T> the return type
     * @param a the Appendable object, may not be <code>null</code>
     * @return the given Appendable object, never <code>null</code>
     */
    public <T extends Appendable> T getExactly(T a) {
        try {
            boolean prependBlank = false;
            long d = delta;
            if (d >= YEAR) {
                a.append(floor(d, YEAR));
                a.append(' ');
                a.append('y');
                prependBlank = true;
            }
            d %= YEAR;
            if (d >= DAY) {
                if (prependBlank) {
                    a.append(' ');
                }
                a.append(floor(d, DAY));
                a.append(' ');
                a.append('d');
                prependBlank = true;
            }
            d %= DAY;
            if (d >= HOUR) {
                if (prependBlank) {
                    a.append(' ');
                }
                a.append(floor(d, HOUR));
                a.append(' ');
                a.append('h');
                prependBlank = true;
            }
            d %= HOUR;
            if (d >= MINUTE) {
                if (prependBlank) {
                    a.append(' ');
                }
                a.append(floor(d, MINUTE));
                a.append(' ');
                a.append('m');
                prependBlank = true;
            }
            d %= MINUTE;
            if (d >= SECOND) {
                if (prependBlank) {
                    a.append(' ');
                }
                a.append(floor(d, SECOND));
                a.append(' ');
                a.append('s');
                prependBlank = true;
            }
            d %= SECOND;
            if (d > 0) {
                if (prependBlank) {
                    a.append(' ');
                }
                a.append(Integer.toString((int) d));
                a.append(' ');
                a.append('m');
                a.append('s');
            }
        } catch (IOException ex) {
            // What were they thinking...
            logger.warn("An error occurred!", ex);
        }
        return a;
    }

    /**
     * Returns an approximate, human-formatted representation of the time delta.
     * 
     * @return a formatted representation of the time delta, never <code>null</code>
     */
    public String getApproximately() {
        return getApproximately(new StringBuilder()).toString();
    }

    /**
     * Appends an approximate, human-formatted representation of the time delta to the given {@link Appendable} object.
     * 
     * @param <T> the return type
     * @param a the Appendable object, may not be <code>null</code>
     * @return the given Appendable object, never <code>null</code>
     */
    public <T extends Appendable> T getApproximately(T a) {

        try {
            int parts = 0;
            boolean rounded = false;
            boolean prependBlank = false;
            long d = delta;
            long mod = d % YEAR;

            if (mod >= upperCeiling(YEAR)) {
                a.append(ceil(d, YEAR));
                a.append(' ');
                a.append('y');
                ++parts;
                rounded = true;
                prependBlank = true;
            } else if (d >= YEAR) {
                a.append(floor(d, YEAR));
                a.append(' ');
                a.append('y');
                ++parts;
                rounded = mod <= lowerCeiling(YEAR);
                prependBlank = true;
            }

            if (!rounded) {
                d %= YEAR;
                mod = d % DAY;

                if (mod >= upperCeiling(DAY)) {
                    if (prependBlank) {
                        a.append(' ');
                    }
                    a.append(ceil(d, DAY));
                    a.append(' ');
                    a.append('d');
                    ++parts;
                    rounded = true;
                    prependBlank = true;
                } else if (d >= DAY) {
                    if (prependBlank) {
                        a.append(' ');
                    }
                    a.append(floor(d, DAY));
                    a.append(' ');
                    a.append('d');
                    ++parts;
                    rounded = mod <= lowerCeiling(DAY);
                    prependBlank = true;
                }

                if (parts < 2) {
                    d %= DAY;
                    mod = d % HOUR;

                    if (mod >= upperCeiling(HOUR)) {
                        if (prependBlank) {
                            a.append(' ');
                        }
                        a.append(ceil(d, HOUR));
                        a.append(' ');
                        a.append('h');
                        ++parts;
                        rounded = true;
                        prependBlank = true;
                    } else if (d >= HOUR && !rounded) {
                        if (prependBlank) {
                            a.append(' ');
                        }
                        a.append(floor(d, HOUR));
                        a.append(' ');
                        a.append('h');
                        ++parts;
                        rounded = mod <= lowerCeiling(HOUR);
                        prependBlank = true;
                    }

                    if (parts < 2) {
                        d %= HOUR;
                        mod = d % MINUTE;

                        if (mod >= upperCeiling(MINUTE)) {
                            if (prependBlank) {
                                a.append(' ');
                            }
                            a.append(ceil(d, MINUTE));
                            a.append(' ');
                            a.append('m');
                            ++parts;
                            rounded = true;
                            prependBlank = true;
                        } else if (d >= MINUTE && !rounded) {
                            if (prependBlank) {
                                a.append(' ');
                            }
                            a.append(floor(d, MINUTE));
                            a.append(' ');
                            a.append('m');
                            ++parts;
                            rounded = mod <= lowerCeiling(MINUTE);
                            prependBlank = true;
                        }

                        if (parts < 2) {
                            d %= MINUTE;
                            mod = d % SECOND;

                            if (mod >= upperCeiling(SECOND)) {
                                if (prependBlank) {
                                    a.append(' ');
                                }
                                a.append(ceil(d, SECOND));
                                a.append(' ');
                                a.append('s');
                                ++parts;
                                rounded = true;
                                prependBlank = true;
                            } else if (d >= SECOND && !rounded) {
                                if (prependBlank) {
                                    a.append(' ');
                                }
                                a.append(floor(d, SECOND));
                                a.append(' ');
                                a.append('s');
                                ++parts;
                                rounded = mod <= lowerCeiling(SECOND);
                                prependBlank = true;
                            }

                            if (parts < 2) {
                                d %= SECOND;

                                if (d > 0 && !rounded) {
                                    if (prependBlank) {
                                        a.append(' ');
                                    }
                                    a.append(Integer.toString((int) d));
                                    a.append(' ');
                                    a.append('m');
                                    a.append('s');
                                }
                            }

                        }

                    }

                }
            }
        } catch (IOException ex) {
            // What were they thinking...
            logger.warn("An error occurred!", ex);
        }

        return a;
    }

    /**
     * Returns the time delta.
     * 
     * @return the time delta
     */
    public long getDelta() {
        return delta;
    }

    /**
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (!(obj instanceof HumanTime)) {
            return false;
        }
        return delta == ((HumanTime) obj).delta;
    }

    /**
     * Returns a 32-bit representation of the time delta.
     * 
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {
        return (int) (delta ^ (delta >> 32));
    }

    /**
     * Returns a String representation of this.
     * <p>
     * The output is identical to {@link #getExactly()}.
     * 
     * @see java.lang.Object#toString()
     * @see #getExactly()
     * @return a String, never <code>null</code>
     */
    @Override
    public String toString() {
        return getExactly();
    }

    /**
     * Compares this HumanTime to another HumanTime.
     * 
     * @param t the other instance, may not be <code>null</code>
     * @return which one is greater
     */
    public int compareTo(HumanTime t) {
        return delta == t.delta ? 0 : (delta < t.delta ? -1 : 1);
    }

    /**
     * Deep-clones this object.
     * 
     * @see java.lang.Object#clone()
     * @throws CloneNotSupportedException
     */
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    /**
     * @see java.io.Externalizable#readExternal(java.io.ObjectInput)
     */
    public void readExternal(ObjectInput in) throws IOException {
        delta = in.readLong();
    }

    /**
     * @see java.io.Externalizable#writeExternal(java.io.ObjectOutput)
     */
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeLong(delta);
    }

}