From 243a8a6f4fd51dbd6b8fb4ead473f614d79bc45f Mon Sep 17 00:00:00 2001 From: dzmipt Date: Fri, 6 Dec 2024 19:37:12 +0100 Subject: [PATCH] Change how we display temporal type units in the Charts --- src/studio/kdb/K.java | 102 +++++++++++++++++++++------ src/studio/kdb/KFormat.java | 115 +++++++++++++++++++++++++++++++ src/studio/kdb/KType.java | 2 + src/studio/ui/chart/KFormat.java | 89 ------------------------ test/studio/kdb/KTest.java | 78 ++++++++++++++++++++- 5 files changed, 273 insertions(+), 113 deletions(-) create mode 100644 src/studio/kdb/KFormat.java delete mode 100644 src/studio/ui/chart/KFormat.java diff --git a/src/studio/kdb/K.java b/src/studio/kdb/K.java index 109410e..a9aff76 100755 --- a/src/studio/kdb/K.java +++ b/src/studio/kdb/K.java @@ -7,7 +7,7 @@ import java.sql.Timestamp; import java.text.DecimalFormat; import java.text.SimpleDateFormat; -import java.time.Clock; +import java.time.*; import java.time.temporal.ChronoUnit; import java.util.*; import java.util.stream.Stream; @@ -17,11 +17,26 @@ public class K { private final static SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy.MM.dd"); private final static SimpleDateFormat dateTimeFormatter = new SimpleDateFormat("yyyy.MM.dd'T'HH:mm:ss.SSS"); private final static SimpleDateFormat timestampFormatter = new SimpleDateFormat("yyyy.MM.dd'D'HH:mm:ss."); - private final static long NS_IN_SEC = 1_000_000_000; - private final static long SEC_IN_DAY = 24 * 60 * 60; - private final static long MS_IN_DAY = 1000 * SEC_IN_DAY; - private final static long NS_IN_DAY = NS_IN_SEC * SEC_IN_DAY; - private final static long NS_IN_MONTH = (long) (NS_IN_DAY*(365*4+1)/(12*4.0)); + private static final java.text.DecimalFormat i2Formatter = new java.text.DecimalFormat("00"); + private static final java.text.DecimalFormat i3Formatter = new java.text.DecimalFormat("000"); + + private static String i2(int i) { + return i2Formatter.format(i); + } + private static String i3(int i) { + return i3Formatter.format(i); + } + private static String l2(long i) { + return i2Formatter.format(i); + } + + public final static long NS_IN_SEC = 1_000_000_000; + public final static long NS_IN_MLS = 1_000_000; + public final static long NS_IN_MIN = 60 * NS_IN_SEC; + public final static long SEC_IN_DAY = 24 * 60 * 60; + public final static long MS_IN_DAY = 1000 * SEC_IN_DAY; + public final static long NS_IN_DAY = NS_IN_SEC * SEC_IN_DAY; + public final static long NS_IN_MONTH = (long) (NS_IN_DAY*(365*4+1)/(12*4.0)); static { TimeZone gmtTimeZone = java.util.TimeZone.getTimeZone("GMT"); @@ -934,6 +949,11 @@ public Date toDate() { public KTimestamp toTimestamp() { return new KTimestamp(value * NS_IN_DAY); } + + public KTimestamp toTimestamp(double fraction) { + return new KTimestamp((long) (value + fraction) * NS_IN_DAY); + } + } public static class KGuid extends KBase { @@ -1007,6 +1027,30 @@ public Time toTime() { } } + // Artificial class need to represent time in the charts. + public static class KTimeLong extends KLongBase { + + public KTimeLong(long value) { + super(KType.TimeLong, value); + } + + @Override + public StringBuilder format(StringBuilder builder, KFormatContext context) { + builder = super.format(builder, context); + long ns = value % NS_IN_SEC; + long v = value / NS_IN_SEC; + long sec = v % 60; + v = v / 60; + long min = v % 60; + long hh = v / 60; + builder.append(l2(hh)) + .append(':').append(i2((int)min)) + .append(':').append(i2((int)sec)) + .append(".").append(nsFormatter.format(ns)); + return builder; + } + } + public static class KDatetime extends KDoubleBase { public KDatetime(double time) { @@ -1040,17 +1084,34 @@ public static class KTimestamp extends KLongBase { static KTimestamp now(Clock clock) { long epoch = clock.instant().toEpochMilli(); long offset = TimeZone.getTimeZone(clock.getZone()).getOffset(epoch); - return new K.KTimestamp( (epoch + offset - MILLIS_OFFSET) * 1_000_000); + return new K.KTimestamp( (epoch + offset - MILLIS_OFFSET) * NS_IN_MLS); + } + + public static KTimestamp of(LocalDateTime dateTime) { + Instant instant = dateTime.toInstant(ZoneOffset.UTC); + long ns = instant.getEpochSecond() * NS_IN_SEC + instant.getNano() - MILLIS_OFFSET * NS_IN_MLS; + return new KTimestamp(ns); } public static KTimestamp now() { return now(systemClock); } + /** + * Equivalent to t2 - this + */ public KTimespan span(KTimestamp t2) { return new KTimespan(t2.value - value); } + public KTimestamp add(Duration duration) { + return new KTimestamp( value + duration.getNano() + NS_IN_SEC * duration.getSeconds() ); + } + + public KTimestamp add(K.KTimespan duration) { + return new KTimestamp( value + duration.value); + } + public KTimestamp(long time) { super(KType.Timestamp, time); } @@ -1246,6 +1307,12 @@ public Date toDate() { cal.set(y, m, 1); return cal.getTime(); } + + public LocalDateTime toLocalDateTime() { + int m = value + 24000, y = m / 12; + m %= 12; + return LocalDateTime.of(y, m+1, 1,0,0); + } } //@TODO: rename to Minute @@ -1314,14 +1381,14 @@ public static class KTimespan extends KLongBase { public final static KTimespan NULL = new KTimespan(NULL_VALUE); public final static KTimespan ZERO = new KTimespan(0); - private final static Map NS_IN_TYPES = Map.of( + final static Map NS_IN_TYPES = Map.of( KType.Timestamp, 1L, KType.Timespan, 1L, KType.Date, NS_IN_DAY, KType.Month, NS_IN_MONTH, - KType.Minute, 60 * NS_IN_SEC, + KType.Minute, NS_IN_MIN, KType.Second, NS_IN_SEC, - KType.Time, 1_000_000L, + KType.Time, NS_IN_MLS, KType.Datetime, NS_IN_DAY ); @@ -1373,6 +1440,10 @@ public double toUnitValue(ChronoUnit unit) { return value / (double) NS_IN_UNITS.get(unit); } + public static KTimespan of(Duration duration) { + return new KTimespan(duration.getSeconds()*NS_IN_SEC + duration.getNano()); + } + public KTimespan(long x) { super(KType.Timespan, x); } @@ -1409,17 +1480,6 @@ public Time toTime() { } - private static final java.text.DecimalFormat i2Formatter = new java.text.DecimalFormat("00"); - private static final java.text.DecimalFormat i3Formatter = new java.text.DecimalFormat("000"); - - private static String i2(int i) { - return i2Formatter.format(i); - } - - private static String i3(int i) { - return i3Formatter.format(i); - } - public static abstract class KBaseVector extends KBase { protected Object array; private final int length; diff --git a/src/studio/kdb/KFormat.java b/src/studio/kdb/KFormat.java new file mode 100644 index 0000000..a3bb25a --- /dev/null +++ b/src/studio/kdb/KFormat.java @@ -0,0 +1,115 @@ +package studio.kdb; + +import java.text.DecimalFormat; +import java.text.FieldPosition; +import java.text.NumberFormat; +import java.text.ParsePosition; +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; + +public class KFormat extends NumberFormat { + + private KType type; + + private static final DecimalFormat fractionFormat = new DecimalFormat("+0.#####;-0.#####"); + + public KFormat(KType type) { + this.type = type; + } + + @Override + public StringBuffer format(long number, StringBuffer toAppendTo, FieldPosition pos) { + return format((double)number, toAppendTo, pos); + } + + @Override + public Number parse(String source, ParsePosition parsePosition) { + throw new UnsupportedOperationException(); + } + + @Override + public StringBuffer format(double value, StringBuffer toAppendTo, FieldPosition pos) { + K.KBase kValue; + if (type == KType.Int || + type == KType.Double || + type == KType.Float || + type == KType.Short || + type == KType.Long) { + kValue = new K.KDouble(value); + } else if (type == KType.Datetime) { + kValue = new K.KDatetime(value); + } else if (type == KType.Timestamp) { + kValue = new K.KTimestamp(getLong(value)); + } else if (type == KType.Timespan) { + kValue = new K.KTimespan(getLong(value)); + } else { + int intValue = getInt(value); + double fraction = value - intValue; + boolean hasFraction = Math.abs(fraction) >= 1e-5; + + if (type == KType.Date) { + kValue = new K.KDate(intValue); + if (hasFraction) { + kValue = ((K.KDate)kValue).toTimestamp(fraction); + } + } else if (type == KType.Month) { + kValue = new K.Month(intValue); + if (hasFraction) { + LocalDateTime dateTime = ((K.Month)kValue).toLocalDateTime(); + + LocalDateTime next = dateTime.plus((int)Math.signum(fraction), ChronoUnit.MONTHS); + long days = Duration.between(dateTime, next).toDays(); + long ns = (long) (Math.abs(fraction) * days * K.NS_IN_DAY); + kValue = K.KTimestamp.of(dateTime).add(new K.KTimespan(ns)); + } + + } else if (type == KType.Time) { + if (hasFraction) { + kValue = new K.KTimeLong((long) (value * K.NS_IN_MLS)); + } else { + kValue = new K.KTime(intValue); + } + System.out.printf("KFormat KTime: value: %f, hasFraction: %b; kValue: %s\n", value, hasFraction, kValue); + } else if (type == KType.Second) { + if (hasFraction) { + kValue = new K.KTimeLong((long) (value * K.NS_IN_SEC)); + } else { + kValue = new K.Second(intValue); + } + } else if (type == KType.Minute) { + if (hasFraction) { + kValue = new K.KTimeLong((long) (value * K.NS_IN_MIN)); + } else { + kValue = new K.Minute(intValue); + } + } else { + throw new IllegalArgumentException("Unsupported type: " + type); + } + } + + toAppendTo.append(kValue.toString(KFormatContext.NO_TYPE)); + + return toAppendTo; + } + + private static long getLong(double value) { + return Math.round(value); + } + + private static int getInt(double value) { + long longValue = getLong(value); + if (longValue >= Integer.MAX_VALUE) return Integer.MAX_VALUE; + if (longValue <= -Integer.MAX_VALUE) return -Integer.MAX_VALUE; + return (int) longValue; + } + + public static double getValue(KType unit, double value) { + return value / K.KTimespan.NS_IN_TYPES.get(unit); + } + + public static double getValue(KType unit, long value) { + return value / (double) K.KTimespan.NS_IN_TYPES.get(unit); + } + +} diff --git a/src/studio/kdb/KType.java b/src/studio/kdb/KType.java index 1d84d92..3ee2307 100644 --- a/src/studio/kdb/KType.java +++ b/src/studio/kdb/KType.java @@ -20,6 +20,8 @@ public enum KType { Second(-18, "second", 'v'), Time(-19, "time", 't'), + TimeLong(-19000,"timeLong", 'l'), + List(0), BooleanVector(Boolean, true), GuidVector(Guid), diff --git a/src/studio/ui/chart/KFormat.java b/src/studio/ui/chart/KFormat.java deleted file mode 100644 index 93bec6b..0000000 --- a/src/studio/ui/chart/KFormat.java +++ /dev/null @@ -1,89 +0,0 @@ -package studio.ui.chart; - -import studio.kdb.K; -import studio.kdb.KFormatContext; -import studio.kdb.KType; - -import java.text.DecimalFormat; -import java.text.FieldPosition; -import java.text.NumberFormat; -import java.text.ParsePosition; - -public class KFormat extends NumberFormat { - - private KType type; - - private static final DecimalFormat fractionFormat = new DecimalFormat("+0.#####;-0.#####"); - - public KFormat(KType type) { - this.type = type; - } - - @Override - public StringBuffer format(long number, StringBuffer toAppendTo, FieldPosition pos) { - return format((double)number, toAppendTo, pos); - } - - @Override - public Number parse(String source, ParsePosition parsePosition) { - throw new UnsupportedOperationException(); - } - - @Override - public StringBuffer format(double value, StringBuffer toAppendTo, FieldPosition pos) { - boolean addFraction = true; - K.KBase kValue; - if (type == KType.Int || - type == KType.Double || - type == KType.Float || - type == KType.Short || - type == KType.Long) { - kValue = new K.KDouble(value); - addFraction = false; - } else if (type == KType.Datetime) { - kValue = new K.KDatetime(value); - addFraction = false; - } else if (type == KType.Date) { - kValue = new K.KDate(getInt(value)); - } else if (type == KType.Time) { - kValue = new K.KTime(getInt(value)); - } else if (type == KType.Timestamp) { - kValue = new K.KTimestamp(getLong(value)); - } else if (type == KType.Timespan) { - kValue = new K.KTimespan(getLong(value)); - } else if (type == KType.Month) { - kValue = new K.Month(getInt(value)); - } else if (type == KType.Second) { - kValue = new K.Second(getInt(value)); - } else if (type == KType.Minute) { - kValue = new K.Minute(getInt(value)); - } else { - throw new IllegalArgumentException("Unsupported type: " + type); - } - - toAppendTo.append(kValue.toString(KFormatContext.NO_TYPE)); - - if (addFraction) { - double fraction = value - Math.floor(value); - if (Math.abs(fraction) >= 1e-5) { - if (value < 0) { - fraction = fraction - 1; - } - toAppendTo.append(fractionFormat.format(fraction)); - } - } - - return toAppendTo; - } - - private static long getLong(double value) { - return (long) value; - } - - private static int getInt(double value) { - if (value >= Integer.MAX_VALUE) return Integer.MAX_VALUE; - if (value <= -Integer.MAX_VALUE) return -Integer.MAX_VALUE; - return (int) value; - } - -} diff --git a/test/studio/kdb/KTest.java b/test/studio/kdb/KTest.java index fbe5a61..b964578 100644 --- a/test/studio/kdb/KTest.java +++ b/test/studio/kdb/KTest.java @@ -3,9 +3,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import java.time.Clock; -import java.time.Instant; -import java.time.ZoneId; +import java.time.*; import java.util.Calendar; import java.util.UUID; @@ -433,4 +431,78 @@ public void testElementClass() { assertNull(list.getType().getElementType()); } + + @Test + public void testLocalDateTimeToTimestamp() { + LocalDateTime localDateTime = + LocalDateTime.of(2024,12,6,17,10,53,123456789); + K.KTimestamp k = K.KTimestamp.of(localDateTime); + assertEquals("2024.12.06D17:10:53.123456789", k.toString()); + } + + @Test + public void testMonthToLocalDateTime() { + K.Month month = new K.Month(299); + assertEquals("2024.12m", month.toString()); + + LocalDateTime expected = LocalDateTime.of(2024,12,1,0,0); + LocalDateTime actual = month.toLocalDateTime(); + assertEquals(expected,actual); + } + + @Test + public void testTimestampAddDuration() { + LocalDateTime localDateTime = + LocalDateTime.of(2024,12,6,17,10,53,123456789); + K.KTimestamp k = K.KTimestamp.of(localDateTime); + + + K.KTimestamp k2 = k.add(Duration.ofDays(1) + .plusHours(1) + .plusMinutes(3) + .plusSeconds(2) + .plusNanos(123456789) ); + + assertEquals("2024.12.07D18:13:55.246913578", k2.toString()); + } + + @Test + public void testTimestampAddTimespan() { + LocalDateTime localDateTime = + LocalDateTime.of(2024,12,6,17,10,53,123456789); + K.KTimestamp k1 = K.KTimestamp.of(localDateTime); + + localDateTime = + LocalDateTime.of(2024,12,7,18,12,56,234567899); + K.KTimestamp k2 = K.KTimestamp.of(localDateTime); + + K.KTimespan span = k1.span(k2); + assertEquals("1D01:02:03.111111110", span.toString()); + + assertEquals(k2, k1.add(span)); + } + + @Test + public void testDurationToTimespan() { + Duration duration = Duration.ofDays(1) + .plusHours(1) + .plusMinutes(3) + .plusSeconds(2) + .plusNanos(123456789); + K.KTimespan span = K.KTimespan.of(duration); + assertEquals("1D01:03:02.123456789", span.toString()); + } + + @Test + public void testKTimeLong() { + Duration duration = Duration.ofDays(1) + .plusHours(1) + .plusMinutes(3) + .plusSeconds(2) + .plusNanos(123456789); + K.KTimespan span = K.KTimespan.of(duration); + long value = span.toLong(); + K.KTimeLong k = new K.KTimeLong(value); + assertEquals("25:03:02.123456789" ,k.toString()); + } }