1

I have a messaging system, and I have a (unwritten) method for a Message class where I want to be able to return an eloquent string representing the two most significant time differences between two points:

Message(String sender, String channel, String message, String target) {
    this.sender = sender;
    this.channel = channel;
    this.message = message;
    this.target = target;
    this.timestamp = System.nanoTime();
}

private String getTime() {
    long now = System.nanoTime();
    return "";
}

public String toString() {
    return String.format("(%s ago)[%s] %s => %s", this.getTime(), this.channel, this.sender, this.message);
}

I'd rather not loop through the TimeUnit enum for different values, and get the largest two non-zero values. Is there a simpler method for retrieving a user-friendly time between two points? Does Java 8's new time API introduce anything for this?

Keep in mind, I don't want a specific date, but the difference of the two.

Rogue
  • 11,105
  • 5
  • 45
  • 71
  • 1
    What do you mean by "eloquent string" and "user-friendly time"? Do you mean words spelled out, such as "two hours and three minutes"? Do you mean a compact standard notation such as "PT2H3M"? – Basil Bourque Apr 17 '14 at 05:00
  • @BasilBourque Something simple as "3 minutes, 29 seconds". If I can get the amount/units to work with, I can most likely make a string from there. – Rogue Apr 17 '14 at 05:02
  • possible duplicate of [Joda-Time: Period to string](http://stackoverflow.com/questions/1440557/joda-time-period-to-string). The [`PeriodFormatterBuilder`](http://www.joda.org/joda-time/apidocs/org/joda/time/format/PeriodFormatterBuilder.html) class in [Joda-Time](http://www.joda.org/joda-time/) enables you to build a descriptive phrase from a date-time object, such as "15 years and 8 months". – Basil Bourque Apr 17 '14 at 05:04
  • I'm not using joda time (or any external libs) – Rogue Apr 17 '14 at 05:05

3 Answers3

1

When you say "I'd rather not loop through the TimeUnit enum for different values", I guess you mean "I'd like someone else to do it for me", because I don't think there is any other way.

What you could do is rely on Duration or Period, depending on long the durations can be in your use cases. You can ask these classes to generate the string for you, they will contain only the significant units, and convert it with a regexp or something if the format does not suit you.

For example:

private String getTime() {
    long now = System.nanoTime();
    Duration d = Duration.ofNanos(now - this.timestamp);
    String durStr = d.toString()
       .replaceFirst("PT", "")
       .replaceFirst("H", " hours ")
       .replaceFirst("M", " minutes ")
       .replaceFirst("S", " seconds");
    return durStr;
}

If the durations can be from very short to very long, you may have to combine the two classes. But at this point it might be just as easy to write the code yourself...

Djizeus
  • 4,161
  • 1
  • 24
  • 42
  • Hm, so how would I go about only retrieving the "top two" results of a `Duration` object then? I don't see anything suggesting in its javadoc that it limits it in such a way. – Rogue Apr 17 '14 at 05:20
  • Hmm true I missed that, then I think you will have to iterate (which is already almost the case in my answer...) or use Joda-Time... You can still save some code I think if you use `StringTokeniser` to parse the formatted duration or period and iterate through the tokens with a limit of 2. – Djizeus Apr 17 '14 at 05:34
1

So, without using any external libraries at all, I managed to put together a somewhat gross / verbose solution. It works flawlessly, but doesn't mean I can't hate it :)

private String getTime() {
    long diff = System.nanoTime() - this.timestamp;
    TimePoint point = this.getTimePoint(diff);
    if (point.getNext() != null) {
        return String.format("%d %s, %d %s", point.getTime(), point.getUnit().toString(),
                point.getNext().getTime(), point.getNext().getUnit().toString());
    } else {
        return String.format("%d %s", point.getTime(), point.getUnit().toString());
    }
}

private static class TimePoint {

    private final long time;
    private final TimeUnit unit;
    private TimePoint next;

    public TimePoint(long time, TimeUnit unit, TimePoint next) {
        this.time = time;
        this.unit = unit;
        this.next = next;
    }

    public long getTime() {
        return this.time;
    }

    public TimeUnit getUnit() {
        return this.unit;
    }

    public TimePoint getNext() {
        return this.next;
    }

    public TimePoint setNext(TimePoint next) {
        this.next = next;
        return this;
    }
}

/**
 * Absolutely disgusting method of retrieving the largest non-zero times
 * difference in a nanosecond period. Forgive me for my sins
 *
 * @since 1.0.0
 * @version 1.0.0
 *
 * @param diff The difference in nanoseconds between two points
 * @return A new {@link TimePoint} representing the largest represented time
 */
private TimePoint getTimePoint(long diff) {
    if (diff < 0) {
        return null;
    }
    long temp;
    TimeUnit u = TimeUnit.NANOSECONDS;
    TimePoint root = null;
    if ((temp = u.toDays(diff)) > 0) {
        root = new TimePoint(temp, TimeUnit.DAYS, null);
        diff -= root.getUnit().toNanos(root.getTime());
    }
    if ((temp = u.toHours(diff)) > 0 || (root != null && temp >= 0)) {
        TimePoint p = new TimePoint(temp, TimeUnit.HOURS, null);
        root = this.allocateNodes(root, p);
        diff -= p.getUnit().toNanos(p.getTime());
    }
    if ((temp = u.toMinutes(diff)) > 0 || (root != null && temp >= 0)) {
        TimePoint p = new TimePoint(temp, TimeUnit.MINUTES, null);
        root = this.allocateNodes(root, p);
        diff -= p.getUnit().toNanos(p.getTime());
    }
    if ((temp = u.toSeconds(diff)) > 0 || (root != null && temp >= 0)) {
        TimePoint p = new TimePoint(temp, TimeUnit.SECONDS, null);
        root = this.allocateNodes(root, p);
        diff -= p.getUnit().toNanos(p.getTime());
    }
    if ((temp = u.toMillis(diff)) > 0 || (root != null && temp >= 0)) {
        TimePoint p = new TimePoint(temp, TimeUnit.MILLISECONDS, null);
        root = this.allocateNodes(root, p);
        diff -= p.getUnit().toNanos(p.getTime());
    }
    if ((temp = u.toMicros(diff)) > 0 || (root != null && temp >= 0)) {
        TimePoint p = new TimePoint(temp, TimeUnit.MICROSECONDS, null);
        root = this.allocateNodes(root, p);
        diff -= p.getUnit().toNanos(p.getTime());
    }
    if (diff >= 0 || (root != null && temp >= 0)) {
        TimePoint p = new TimePoint(temp, TimeUnit.NANOSECONDS, null);
        root = this.allocateNodes(root, p);
        diff -= p.getUnit().toNanos(p.getTime());
    }
    return root;
}

private TimePoint allocateNodes(TimePoint root, TimePoint allocate) {
    if (root == null) {
        return allocate;
    } else if (root.getNext() != null) {
        return root.setNext(this.allocateNodes(root.getNext(), allocate));
    } else {
        return root.setNext(allocate);
    }
}

How this works:

A TimePoint is a single point representing an amount of time, and the passed TimeUnit. It will also have a getNext which will return a set next value. The boolean check root != null && temp >= 0 is made so that it will only add values that are 0 if there is already a root node (this way the whole chain of TimePoint objects would read "1 minute, 0 seconds, 19 milliseconds" etc).

allocateNodes simply recursively tracks down a chain of TimePoints until it can find a null node, and sets it there.

Without TimeUnit having something of the sort of from<Unit>, I could not really think of any viable way of doing this without writing it completely out, however that part has been done now.

Rogue
  • 11,105
  • 5
  • 45
  • 71
0

The Oracle Tutorial for the new java.time package in Java 8 gives this example code using the Period class

Period p = Period.between(birthday, today);
long p2 = ChronoUnit.DAYS.between(birthday, today);
System.out.println("You are " + p.getYears() + " years, " + p.getMonths() +
               " months, and " + p.getDays() +
               " days old. (" + p2 + " days total)");

The Joda-Time library offers the PeriodFormatterBuilder class to assist in that string-building. But I do not see the equivalent in java.time.

Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
  • OP does not want to print the units that have a zero value. – Djizeus Apr 17 '14 at 05:14
  • That seems to be as good as java.time gets for this problem. The OP will have to test for zero values to exclude them from the string. While java.time has some good features, Joda-Time still outshines it in some ways. This is one of those ways. – Basil Bourque Apr 17 '14 at 05:19