package org.embulk.spi.time; import java.util.regex.Pattern; import java.util.regex.Matcher; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import org.joda.time.DateTime; import org.jruby.Ruby; import org.jruby.RubyTime; public class Timestamp implements Comparable<Timestamp> { private final static DateTimeFormatter TO_STRING_FORMATTER_SECONDS = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss ").withZoneUTC(); private final static DateTimeFormatter TO_STRING_FORMATTER_MILLIS = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss.SSS ").withZoneUTC(); private final static DateTimeFormatter TO_STRING_FORMATTER_CUSTOM = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss").withZoneUTC(); private static final Pattern FROM_STRING_PATTERN = Pattern.compile("(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})(?:\\.(\\d{1,9}))? (?:UTC|\\+?00\\:?00)"); private final long seconds; private final int nano; private Timestamp(long seconds, int nano) { this.seconds = seconds; this.nano = nano; } public static Timestamp ofEpochSecond(long epochSecond) { return new Timestamp(epochSecond, 0); } public static Timestamp ofEpochSecond(long epochSecond, long nanoAdjustment) { return new Timestamp(epochSecond + nanoAdjustment / 1000000000, (int) (nanoAdjustment % 1000000000)); } public static Timestamp ofEpochMilli(long epochMilli) { return new Timestamp(epochMilli / 1000, (int) (epochMilli % 1000 * 1000000)); } public long getEpochSecond() { return seconds; } public int getNano() { return nano; } public long toEpochMilli() { return seconds * 1000 + nano / 1000000; } @Override public boolean equals(Object other) { if (this == other) { return true; } if (!(other instanceof Timestamp)) { return false; } Timestamp o = (Timestamp) other; return this.seconds == o.seconds && this.nano == o.nano; } @Override public int hashCode() { int h = (int) (seconds ^ (seconds >>> 32)); h += 17 * nano; return h; } @Override public int compareTo(Timestamp t) { if (seconds < t.seconds) { return -1; } else if (seconds == t.seconds) { return nano == t.nano ? 0 : (nano < t.nano ? -1 : 1); } else { return 1; } } public RubyTime getRubyTime(Ruby runtime) { RubyTime time = new RubyTime(runtime, runtime.getClass("Time"), new DateTime(toEpochMilli())).gmtime(); time.setNSec(nano % 1000000); return time; } public static Timestamp fromRubyTime(RubyTime time) { long msec = time.getDateTime().getMillis(); long sec = msec / 1000; long nsec = time.getNSec() + (msec % 1000) * 1000000; return Timestamp.ofEpochSecond(sec, nsec); } @Override public String toString() { if (nano == 0) { return TO_STRING_FORMATTER_SECONDS.print(getEpochSecond() * 1000) + "UTC"; } else if (nano % 1000000 == 0) { return TO_STRING_FORMATTER_MILLIS.print(toEpochMilli()) + "UTC"; } else { StringBuffer sb = new StringBuffer(); TO_STRING_FORMATTER_CUSTOM.printTo(sb, getEpochSecond() * 1000); sb.append("."); String digits; int zeroDigits; if (nano % 1000 == 0) { digits = Integer.toString(nano / 1000); zeroDigits = 6 - digits.length(); } else { digits = Integer.toString(nano); zeroDigits = 9 - digits.length(); } sb.append(digits); for (; zeroDigits > 0; zeroDigits--) { sb.append('0'); } sb.append(" UTC"); return sb.toString(); } } static Timestamp fromString(String text) { // TODO exception handling Matcher m = FROM_STRING_PATTERN.matcher(text); if (!m.matches()) { throw new IllegalArgumentException(String.format("Invalid timestamp format '%s'", text)); } long seconds = TO_STRING_FORMATTER_CUSTOM.parseDateTime(m.group(1)).getMillis() / 1000; int nano; String frac = m.group(2); if (frac == null) { nano = 0; } else { nano = Integer.parseInt(frac) * (int) Math.pow(10, 9 - frac.length()); } return new Timestamp(seconds, nano); } }