From 229f23d6a3f21afa8a0ef1f3fbbd934e6f4b400e Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 20 Mar 2026 09:19:03 +0200 Subject: [PATCH 1/3] Adding support for java.time API --- Ports/CLDC11/src/java/time/Clock.java | 57 ++++ .../CLDC11/src/java/time/DateTimeSupport.java | 228 ++++++++++++++++ Ports/CLDC11/src/java/time/Duration.java | 91 +++++++ Ports/CLDC11/src/java/time/Instant.java | 93 +++++++ Ports/CLDC11/src/java/time/LocalDate.java | 135 ++++++++++ Ports/CLDC11/src/java/time/LocalDateTime.java | 141 ++++++++++ Ports/CLDC11/src/java/time/LocalTime.java | 131 +++++++++ .../CLDC11/src/java/time/OffsetDateTime.java | 73 +++++ Ports/CLDC11/src/java/time/Period.java | 59 ++++ Ports/CLDC11/src/java/time/ZoneId.java | 58 ++++ Ports/CLDC11/src/java/time/ZoneOffset.java | 92 +++++++ Ports/CLDC11/src/java/time/ZonedDateTime.java | 94 +++++++ .../java/time/format/DateTimeFormatter.java | 246 +++++++++++++++++ .../time/format/DateTimeParseException.java | 20 ++ .../java/time/temporal/TemporalAccessor.java | 4 + Ports/CLDC11/src/java/util/TimeZone.java | 3 + .../tests/Cn1ssDeviceRunner.java | 1 + .../hellocodenameone/tests/TimeApiTest.java | 69 +++++ vm/ByteCodeTranslator/src/nativeMethods.m | 233 ++++++++++++++++ vm/JavaAPI/src/java/time/Clock.java | 57 ++++ vm/JavaAPI/src/java/time/DateTimeSupport.java | 252 ++++++++++++++++++ vm/JavaAPI/src/java/time/Duration.java | 91 +++++++ vm/JavaAPI/src/java/time/Instant.java | 93 +++++++ vm/JavaAPI/src/java/time/LocalDate.java | 135 ++++++++++ vm/JavaAPI/src/java/time/LocalDateTime.java | 141 ++++++++++ vm/JavaAPI/src/java/time/LocalTime.java | 131 +++++++++ vm/JavaAPI/src/java/time/OffsetDateTime.java | 73 +++++ vm/JavaAPI/src/java/time/Period.java | 59 ++++ vm/JavaAPI/src/java/time/ZoneId.java | 58 ++++ vm/JavaAPI/src/java/time/ZoneOffset.java | 92 +++++++ vm/JavaAPI/src/java/time/ZonedDateTime.java | 94 +++++++ .../java/time/format/DateTimeFormatter.java | 246 +++++++++++++++++ .../time/format/DateTimeParseException.java | 20 ++ .../java/time/temporal/TemporalAccessor.java | 4 + vm/JavaAPI/src/java/util/TimeZone.java | 4 + .../translator/TimeApiIntegrationTest.java | 153 +++++++++++ .../tools/translator/TimeEdgeApp.java | 67 +++++ 37 files changed, 3598 insertions(+) create mode 100644 Ports/CLDC11/src/java/time/Clock.java create mode 100644 Ports/CLDC11/src/java/time/DateTimeSupport.java create mode 100644 Ports/CLDC11/src/java/time/Duration.java create mode 100644 Ports/CLDC11/src/java/time/Instant.java create mode 100644 Ports/CLDC11/src/java/time/LocalDate.java create mode 100644 Ports/CLDC11/src/java/time/LocalDateTime.java create mode 100644 Ports/CLDC11/src/java/time/LocalTime.java create mode 100644 Ports/CLDC11/src/java/time/OffsetDateTime.java create mode 100644 Ports/CLDC11/src/java/time/Period.java create mode 100644 Ports/CLDC11/src/java/time/ZoneId.java create mode 100644 Ports/CLDC11/src/java/time/ZoneOffset.java create mode 100644 Ports/CLDC11/src/java/time/ZonedDateTime.java create mode 100644 Ports/CLDC11/src/java/time/format/DateTimeFormatter.java create mode 100644 Ports/CLDC11/src/java/time/format/DateTimeParseException.java create mode 100644 Ports/CLDC11/src/java/time/temporal/TemporalAccessor.java create mode 100644 scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/TimeApiTest.java create mode 100644 vm/JavaAPI/src/java/time/Clock.java create mode 100644 vm/JavaAPI/src/java/time/DateTimeSupport.java create mode 100644 vm/JavaAPI/src/java/time/Duration.java create mode 100644 vm/JavaAPI/src/java/time/Instant.java create mode 100644 vm/JavaAPI/src/java/time/LocalDate.java create mode 100644 vm/JavaAPI/src/java/time/LocalDateTime.java create mode 100644 vm/JavaAPI/src/java/time/LocalTime.java create mode 100644 vm/JavaAPI/src/java/time/OffsetDateTime.java create mode 100644 vm/JavaAPI/src/java/time/Period.java create mode 100644 vm/JavaAPI/src/java/time/ZoneId.java create mode 100644 vm/JavaAPI/src/java/time/ZoneOffset.java create mode 100644 vm/JavaAPI/src/java/time/ZonedDateTime.java create mode 100644 vm/JavaAPI/src/java/time/format/DateTimeFormatter.java create mode 100644 vm/JavaAPI/src/java/time/format/DateTimeParseException.java create mode 100644 vm/JavaAPI/src/java/time/temporal/TemporalAccessor.java create mode 100644 vm/tests/src/test/java/com/codename1/tools/translator/TimeApiIntegrationTest.java create mode 100644 vm/tests/src/test/resources/com/codename1/tools/translator/TimeEdgeApp.java diff --git a/Ports/CLDC11/src/java/time/Clock.java b/Ports/CLDC11/src/java/time/Clock.java new file mode 100644 index 0000000000..85d04e71d4 --- /dev/null +++ b/Ports/CLDC11/src/java/time/Clock.java @@ -0,0 +1,57 @@ +package java.time; + +public abstract class Clock { + public abstract ZoneId getZone(); + + public abstract Instant instant(); + + public long millis() { + return instant().toEpochMilli(); + } + + public static Clock systemUTC() { + return new SystemClock(ZoneOffset.UTC); + } + + public static Clock systemDefaultZone() { + return new SystemClock(ZoneId.systemDefault()); + } + + public static Clock fixed(Instant fixedInstant, ZoneId zone) { + return new FixedClock(fixedInstant, zone); + } + + private static final class SystemClock extends Clock { + private final ZoneId zone; + + private SystemClock(ZoneId zone) { + this.zone = zone; + } + + public ZoneId getZone() { + return zone; + } + + public Instant instant() { + return Instant.now(); + } + } + + private static final class FixedClock extends Clock { + private final Instant instant; + private final ZoneId zone; + + private FixedClock(Instant instant, ZoneId zone) { + this.instant = instant; + this.zone = zone; + } + + public ZoneId getZone() { + return zone; + } + + public Instant instant() { + return instant; + } + } +} diff --git a/Ports/CLDC11/src/java/time/DateTimeSupport.java b/Ports/CLDC11/src/java/time/DateTimeSupport.java new file mode 100644 index 0000000000..7d26faa10e --- /dev/null +++ b/Ports/CLDC11/src/java/time/DateTimeSupport.java @@ -0,0 +1,228 @@ +package java.time; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +public final class DateTimeSupport { + static final long MILLIS_PER_SECOND = 1000L; + static final long MILLIS_PER_DAY = 86400000L; + static final long SECONDS_PER_DAY = 86400L; + static final long NANOS_PER_SECOND = 1000000000L; + static final long NANOS_PER_MILLI = 1000000L; + static final long NANOS_PER_DAY = 86400000000000L; + + private static final long DAYS_0000_TO_1970 = 719528L; + + private DateTimeSupport() { + } + + public static int floorDiv(int x, int y) { + int r = x / y; + if ((x ^ y) < 0 && (r * y != x)) { + r--; + } + return r; + } + + public static long floorDiv(long x, long y) { + long r = x / y; + if ((x ^ y) < 0 && (r * y != x)) { + r--; + } + return r; + } + + public static int floorMod(int x, int y) { + return x - floorDiv(x, y) * y; + } + + public static long floorMod(long x, long y) { + return x - floorDiv(x, y) * y; + } + + public static boolean isLeapYear(int year) { + return ((year & 3) == 0) && ((year % 100) != 0 || (year % 400) == 0); + } + + public static int lengthOfMonth(int year, int month) { + switch (month) { + case 2: + return isLeapYear(year) ? 29 : 28; + case 4: + case 6: + case 9: + case 11: + return 30; + default: + return 31; + } + } + + public static long toEpochDay(int year, int month, int dayOfMonth) { + long y = year; + long m = month; + long total = 365L * y; + if (y >= 0) { + total += (y + 3) / 4 - (y + 99) / 100 + (y + 399) / 400; + } else { + total -= y / -4 - y / -100 + y / -400; + } + total += ((367 * m - 362) / 12); + total += dayOfMonth - 1; + if (m > 2) { + total--; + if (!isLeapYear(year)) { + total--; + } + } + return total - DAYS_0000_TO_1970; + } + + public static int[] epochDayToDate(long epochDay) { + long zeroDay = epochDay + DAYS_0000_TO_1970; + zeroDay -= 60; + long adjust = 0; + if (zeroDay < 0) { + long adjustCycles = (zeroDay + 1) / 146097 - 1; + adjust = adjustCycles * 400; + zeroDay += -adjustCycles * 146097; + } + long yearEst = (400 * zeroDay + 591) / 146097; + long doyEst = zeroDay - (365 * yearEst + yearEst / 4 - yearEst / 100 + yearEst / 400); + if (doyEst < 0) { + yearEst--; + doyEst = zeroDay - (365 * yearEst + yearEst / 4 - yearEst / 100 + yearEst / 400); + } + yearEst += adjust; + int marchDoy0 = (int) doyEst; + int marchMonth0 = (marchDoy0 * 5 + 2) / 153; + int month = (marchMonth0 + 2) % 12 + 1; + int day = marchDoy0 - (marchMonth0 * 306 + 5) / 10 + 1; + yearEst += marchMonth0 / 10; + return new int[] { (int) yearEst, month, day }; + } + + public static void checkDate(int year, int month, int day) { + if (month < 1 || month > 12) { + throw new IllegalArgumentException("Invalid month: " + month); + } + int maxDay = lengthOfMonth(year, month); + if (day < 1 || day > maxDay) { + throw new IllegalArgumentException("Invalid day: " + day); + } + } + + public static void checkTime(int hour, int minute, int second, int nano) { + if (hour < 0 || hour > 23) { + throw new IllegalArgumentException("Invalid hour: " + hour); + } + if (minute < 0 || minute > 59) { + throw new IllegalArgumentException("Invalid minute: " + minute); + } + if (second < 0 || second > 59) { + throw new IllegalArgumentException("Invalid second: " + second); + } + if (nano < 0 || nano >= NANOS_PER_SECOND) { + throw new IllegalArgumentException("Invalid nano: " + nano); + } + } + + public static long toEpochSecond(LocalDate date, LocalTime time, ZoneOffset offset) { + long days = date.toEpochDay(); + long secs = days * SECONDS_PER_DAY + time.toSecondOfDay(); + return secs - offset.getTotalSeconds(); + } + + public static int millisOfSecond(int nano) { + return nano / 1000000; + } + + public static Calendar newCalendar(TimeZone tz) { + Calendar out = Calendar.getInstance(tz); + return out; + } + + public static LocalDateTime localDateTimeFromInstant(Instant instant, ZoneId zone) { + Calendar cal = newCalendar(zone.toTimeZone()); + cal.setTime(new Date(instant.toEpochMilli())); + return LocalDateTime.of( + cal.get(Calendar.YEAR), + cal.get(Calendar.MONTH) + 1, + cal.get(Calendar.DAY_OF_MONTH), + cal.get(Calendar.HOUR_OF_DAY), + cal.get(Calendar.MINUTE), + cal.get(Calendar.SECOND), + cal.get(Calendar.MILLISECOND) * 1000000); + } + + public static ZoneOffset offsetFromInstant(Instant instant, ZoneId zone) { + TimeZone tz = zone.toTimeZone(); + Calendar cal = newCalendar(TimeZone.getTimeZone("GMT")); + cal.setTime(new Date(instant.toEpochMilli())); + int offsetMillis = tz.getOffset( + 1, + cal.get(Calendar.YEAR), + cal.get(Calendar.MONTH), + cal.get(Calendar.DAY_OF_MONTH), + cal.get(Calendar.DAY_OF_WEEK), + ((cal.get(Calendar.HOUR_OF_DAY) * 60 + cal.get(Calendar.MINUTE)) * 60 + cal.get(Calendar.SECOND)) * 1000 + + cal.get(Calendar.MILLISECOND)); + return ZoneOffset.ofTotalSeconds(offsetMillis / 1000); + } + + public static SimpleDateFormat newFormat(String pattern, ZoneId zone, Locale locale) { + SimpleDateFormat sdf = new SimpleDateFormat(pattern); + return sdf; + } + + public static String formatPattern(String pattern, TemporalCarrier carrier, Locale locale) { + ZoneId zone = carrier.getZoneForFormatting(); + SimpleDateFormat sdf = newFormat(pattern, zone, locale); + TimeZone original = TimeZone.getDefault(); + try { + if (zone != null) { + TimeZone.setDefault(zone.toTimeZone()); + } + return sdf.format(new Date(carrier.toInstant().toEpochMilli())); + } finally { + TimeZone.setDefault(original); + } + } + + public static ParsedPatternResult parsePattern(String text, String pattern, ZoneId defaultZone, Locale locale) { + TimeZone original = TimeZone.getDefault(); + try { + if (defaultZone != null) { + TimeZone.setDefault(defaultZone.toTimeZone()); + } + SimpleDateFormat sdf = newFormat(pattern, defaultZone, locale); + Date date = sdf.parse(text); + Instant instant = Instant.ofEpochMilli(date.getTime()); + ZoneId zone = defaultZone == null ? ZoneOffset.UTC : defaultZone; + return new ParsedPatternResult(instant, zone); + } catch (ParseException err) { + throw new java.time.format.DateTimeParseException(err.getMessage(), text, 0); + } finally { + TimeZone.setDefault(original); + } + } + + public interface TemporalCarrier { + Instant toInstant(); + ZoneId getZoneForFormatting(); + } + + public static final class ParsedPatternResult { + public final Instant instant; + public final ZoneId zone; + + ParsedPatternResult(Instant instant, ZoneId zone) { + this.instant = instant; + this.zone = zone; + } + } +} diff --git a/Ports/CLDC11/src/java/time/Duration.java b/Ports/CLDC11/src/java/time/Duration.java new file mode 100644 index 0000000000..6532a92cff --- /dev/null +++ b/Ports/CLDC11/src/java/time/Duration.java @@ -0,0 +1,91 @@ +package java.time; + +public final class Duration implements Comparable { + private final long seconds; + private final int nanos; + + private Duration(long seconds, int nanos) { + this.seconds = seconds; + this.nanos = nanos; + } + + public static Duration ofDays(long days) { + return ofSeconds(days * 86400L); + } + + public static Duration ofHours(long hours) { + return ofSeconds(hours * 3600L); + } + + public static Duration ofMinutes(long minutes) { + return ofSeconds(minutes * 60L); + } + + public static Duration ofSeconds(long seconds) { + return new Duration(seconds, 0); + } + + public static Duration ofSeconds(long seconds, long nanoAdjustment) { + long secs = seconds + DateTimeSupport.floorDiv(nanoAdjustment, DateTimeSupport.NANOS_PER_SECOND); + int nanos = (int) DateTimeSupport.floorMod(nanoAdjustment, DateTimeSupport.NANOS_PER_SECOND); + return new Duration(secs, nanos); + } + + public static Duration ofMillis(long millis) { + return ofSeconds(DateTimeSupport.floorDiv(millis, 1000L), DateTimeSupport.floorMod(millis, 1000L) * 1000000L); + } + + public long getSeconds() { + return seconds; + } + + public int getNano() { + return nanos; + } + + public long toMillis() { + return seconds * 1000L + nanos / 1000000L; + } + + public Duration plus(Duration other) { + return ofSeconds(seconds + other.seconds, nanos + other.nanos); + } + + public Duration minus(Duration other) { + return ofSeconds(seconds - other.seconds, nanos - other.nanos); + } + + public int compareTo(Duration other) { + if (seconds != other.seconds) { + return seconds < other.seconds ? -1 : 1; + } + return nanos < other.nanos ? -1 : nanos > other.nanos ? 1 : 0; + } + + public boolean equals(Object obj) { + return obj instanceof Duration && compareTo((Duration) obj) == 0; + } + + public int hashCode() { + return (int) (seconds ^ (seconds >>> 32)) + nanos * 31; + } + + public String toString() { + StringBuffer sb = new StringBuffer("PT"); + long absSeconds = Math.abs(seconds); + if (seconds < 0 && absSeconds > 0) { + sb.append('-'); + } + sb.append(absSeconds); + if (nanos != 0) { + sb.append('.'); + String frac = String.valueOf(1000000000L + nanos).substring(1); + while (frac.endsWith("0")) { + frac = frac.substring(0, frac.length() - 1); + } + sb.append(frac); + } + sb.append('S'); + return sb.toString(); + } +} diff --git a/Ports/CLDC11/src/java/time/Instant.java b/Ports/CLDC11/src/java/time/Instant.java new file mode 100644 index 0000000000..ec76b882dd --- /dev/null +++ b/Ports/CLDC11/src/java/time/Instant.java @@ -0,0 +1,93 @@ +package java.time; + +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; + +public final class Instant implements Comparable, TemporalAccessor, DateTimeSupport.TemporalCarrier { + private final long epochSecond; + private final int nano; + + private Instant(long epochSecond, int nano) { + this.epochSecond = epochSecond; + this.nano = nano; + } + + public static Instant now() { + return ofEpochMilli(System.currentTimeMillis()); + } + + public static Instant ofEpochMilli(long epochMilli) { + long secs = DateTimeSupport.floorDiv(epochMilli, 1000L); + int nanos = (int) (DateTimeSupport.floorMod(epochMilli, 1000L) * 1000000L); + return new Instant(secs, nanos); + } + + public static Instant ofEpochSecond(long epochSecond) { + return new Instant(epochSecond, 0); + } + + public static Instant ofEpochSecond(long epochSecond, long nanoAdjustment) { + long secs = epochSecond + DateTimeSupport.floorDiv(nanoAdjustment, DateTimeSupport.NANOS_PER_SECOND); + int nanos = (int) DateTimeSupport.floorMod(nanoAdjustment, DateTimeSupport.NANOS_PER_SECOND); + return new Instant(secs, nanos); + } + + public static Instant parse(CharSequence text) { + return DateTimeFormatter.ISO_INSTANT.parseInstant(text.toString()); + } + + public long getEpochSecond() { + return epochSecond; + } + + public int getNano() { + return nano; + } + + public long toEpochMilli() { + return epochSecond * 1000L + nano / 1000000L; + } + + public Instant plusSeconds(long secondsToAdd) { + return ofEpochSecond(epochSecond + secondsToAdd, nano); + } + + public Instant plusMillis(long millisToAdd) { + return ofEpochMilli(toEpochMilli() + millisToAdd); + } + + public Instant minusSeconds(long secondsToSubtract) { + return plusSeconds(-secondsToSubtract); + } + + public Instant minusMillis(long millisToSubtract) { + return plusMillis(-millisToSubtract); + } + + public int compareTo(Instant other) { + if (epochSecond != other.epochSecond) { + return epochSecond < other.epochSecond ? -1 : 1; + } + return nano < other.nano ? -1 : nano > other.nano ? 1 : 0; + } + + public boolean equals(Object obj) { + return obj instanceof Instant && compareTo((Instant) obj) == 0; + } + + public int hashCode() { + return (int) (epochSecond ^ (epochSecond >>> 32)) + nano * 51; + } + + public String toString() { + return DateTimeFormatter.ISO_INSTANT.format(this); + } + + public Instant toInstant() { + return this; + } + + public ZoneId getZoneForFormatting() { + return ZoneOffset.UTC; + } +} diff --git a/Ports/CLDC11/src/java/time/LocalDate.java b/Ports/CLDC11/src/java/time/LocalDate.java new file mode 100644 index 0000000000..b633051f3b --- /dev/null +++ b/Ports/CLDC11/src/java/time/LocalDate.java @@ -0,0 +1,135 @@ +package java.time; + +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; + +public final class LocalDate implements Comparable, TemporalAccessor { + private final int year; + private final int month; + private final int dayOfMonth; + + private LocalDate(int year, int month, int dayOfMonth) { + DateTimeSupport.checkDate(year, month, dayOfMonth); + this.year = year; + this.month = month; + this.dayOfMonth = dayOfMonth; + } + + public static LocalDate now() { + return now(Clock.systemDefaultZone()); + } + + public static LocalDate now(Clock clock) { + return LocalDateTime.ofInstant(clock.instant(), clock.getZone()).toLocalDate(); + } + + public static LocalDate of(int year, int month, int dayOfMonth) { + return new LocalDate(year, month, dayOfMonth); + } + + public static LocalDate ofEpochDay(long epochDay) { + int[] parts = DateTimeSupport.epochDayToDate(epochDay); + return of(parts[0], parts[1], parts[2]); + } + + public static LocalDate parse(CharSequence text) { + return DateTimeFormatter.ISO_LOCAL_DATE.parseLocalDate(text.toString()); + } + + public static LocalDate parse(CharSequence text, DateTimeFormatter formatter) { + return formatter.parseLocalDate(text.toString()); + } + + public int getYear() { + return year; + } + + public int getMonthValue() { + return month; + } + + public int getDayOfMonth() { + return dayOfMonth; + } + + public boolean isLeapYear() { + return DateTimeSupport.isLeapYear(year); + } + + public int lengthOfMonth() { + return DateTimeSupport.lengthOfMonth(year, month); + } + + public long toEpochDay() { + return DateTimeSupport.toEpochDay(year, month, dayOfMonth); + } + + public LocalDate plusDays(long daysToAdd) { + return ofEpochDay(toEpochDay() + daysToAdd); + } + + public LocalDate plusMonths(long monthsToAdd) { + long monthCount = year * 12L + (month - 1); + long calcMonths = monthCount + monthsToAdd; + int newYear = (int) DateTimeSupport.floorDiv(calcMonths, 12); + int newMonth = (int) DateTimeSupport.floorMod(calcMonths, 12) + 1; + int newDay = Math.min(dayOfMonth, DateTimeSupport.lengthOfMonth(newYear, newMonth)); + return of(newYear, newMonth, newDay); + } + + public LocalDate plusYears(long yearsToAdd) { + int newYear = (int) (year + yearsToAdd); + int newDay = Math.min(dayOfMonth, DateTimeSupport.lengthOfMonth(newYear, month)); + return of(newYear, month, newDay); + } + + public LocalDate minusDays(long daysToSubtract) { + return plusDays(-daysToSubtract); + } + + public LocalDateTime atTime(int hour, int minute) { + return LocalDateTime.of(this, LocalTime.of(hour, minute)); + } + + public LocalDateTime atTime(int hour, int minute, int second) { + return LocalDateTime.of(this, LocalTime.of(hour, minute, second)); + } + + public LocalDateTime atTime(int hour, int minute, int second, int nano) { + return LocalDateTime.of(this, LocalTime.of(hour, minute, second, nano)); + } + + public LocalDateTime atTime(LocalTime time) { + return LocalDateTime.of(this, time); + } + + public ZonedDateTime atStartOfDay(ZoneId zone) { + return ZonedDateTime.of(this, LocalTime.MIDNIGHT, zone); + } + + public String format(DateTimeFormatter formatter) { + return formatter.format(this); + } + + public int compareTo(LocalDate other) { + if (year != other.year) { + return year < other.year ? -1 : 1; + } + if (month != other.month) { + return month < other.month ? -1 : 1; + } + return dayOfMonth < other.dayOfMonth ? -1 : dayOfMonth > other.dayOfMonth ? 1 : 0; + } + + public boolean equals(Object obj) { + return obj instanceof LocalDate && compareTo((LocalDate) obj) == 0; + } + + public int hashCode() { + return year * 37 + month * 13 + dayOfMonth; + } + + public String toString() { + return DateTimeFormatter.ISO_LOCAL_DATE.format(this); + } +} diff --git a/Ports/CLDC11/src/java/time/LocalDateTime.java b/Ports/CLDC11/src/java/time/LocalDateTime.java new file mode 100644 index 0000000000..b63c452a2c --- /dev/null +++ b/Ports/CLDC11/src/java/time/LocalDateTime.java @@ -0,0 +1,141 @@ +package java.time; + +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; + +public final class LocalDateTime implements Comparable, TemporalAccessor, DateTimeSupport.TemporalCarrier { + private final LocalDate date; + private final LocalTime time; + + private LocalDateTime(LocalDate date, LocalTime time) { + this.date = date; + this.time = time; + } + + public static LocalDateTime now() { + return now(Clock.systemDefaultZone()); + } + + public static LocalDateTime now(Clock clock) { + return ofInstant(clock.instant(), clock.getZone()); + } + + public static LocalDateTime of(LocalDate date, LocalTime time) { + return new LocalDateTime(date, time); + } + + public static LocalDateTime of(int year, int month, int dayOfMonth, int hour, int minute) { + return of(LocalDate.of(year, month, dayOfMonth), LocalTime.of(hour, minute)); + } + + public static LocalDateTime of(int year, int month, int dayOfMonth, int hour, int minute, int second) { + return of(LocalDate.of(year, month, dayOfMonth), LocalTime.of(hour, minute, second)); + } + + public static LocalDateTime of(int year, int month, int dayOfMonth, int hour, int minute, int second, int nano) { + return of(LocalDate.of(year, month, dayOfMonth), LocalTime.of(hour, minute, second, nano)); + } + + public static LocalDateTime ofInstant(Instant instant, ZoneId zone) { + return DateTimeSupport.localDateTimeFromInstant(instant, zone); + } + + public static LocalDateTime parse(CharSequence text) { + return DateTimeFormatter.ISO_LOCAL_DATE_TIME.parseLocalDateTime(text.toString()); + } + + public static LocalDateTime parse(CharSequence text, DateTimeFormatter formatter) { + return formatter.parseLocalDateTime(text.toString()); + } + + public LocalDate toLocalDate() { + return date; + } + + public LocalTime toLocalTime() { + return time; + } + + public int getYear() { + return date.getYear(); + } + + public int getMonthValue() { + return date.getMonthValue(); + } + + public int getDayOfMonth() { + return date.getDayOfMonth(); + } + + public int getHour() { + return time.getHour(); + } + + public int getMinute() { + return time.getMinute(); + } + + public int getSecond() { + return time.getSecond(); + } + + public int getNano() { + return time.getNano(); + } + + public LocalDateTime plusDays(long days) { + return of(date.plusDays(days), time); + } + + public LocalDateTime plusHours(long hours) { + long totalNanos = time.toNanoOfDay() + hours * 3600L * DateTimeSupport.NANOS_PER_SECOND; + long dayAdjust = DateTimeSupport.floorDiv(totalNanos, DateTimeSupport.NANOS_PER_DAY); + long nanoOfDay = DateTimeSupport.floorMod(totalNanos, DateTimeSupport.NANOS_PER_DAY); + return of(date.plusDays(dayAdjust), LocalTime.ofNanoOfDay(nanoOfDay)); + } + + public LocalDateTime plusMinutes(long minutes) { + return plusHours(minutes / 60).plusSeconds((minutes % 60) * 60); + } + + public LocalDateTime plusSeconds(long seconds) { + long totalNanos = time.toNanoOfDay() + seconds * DateTimeSupport.NANOS_PER_SECOND; + long dayAdjust = DateTimeSupport.floorDiv(totalNanos, DateTimeSupport.NANOS_PER_DAY); + long nanoOfDay = DateTimeSupport.floorMod(totalNanos, DateTimeSupport.NANOS_PER_DAY); + return of(date.plusDays(dayAdjust), LocalTime.ofNanoOfDay(nanoOfDay)); + } + + public Instant toInstant(ZoneOffset offset) { + return Instant.ofEpochSecond(DateTimeSupport.toEpochSecond(date, time, offset), time.getNano()); + } + + public String format(DateTimeFormatter formatter) { + return formatter.format(this); + } + + public int compareTo(LocalDateTime other) { + int cmp = date.compareTo(other.date); + return cmp != 0 ? cmp : time.compareTo(other.time); + } + + public boolean equals(Object obj) { + return obj instanceof LocalDateTime && compareTo((LocalDateTime) obj) == 0; + } + + public int hashCode() { + return date.hashCode() * 31 + time.hashCode(); + } + + public String toString() { + return DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(this); + } + + public Instant toInstant() { + return toInstant(ZoneOffset.UTC); + } + + public ZoneId getZoneForFormatting() { + return ZoneOffset.UTC; + } +} diff --git a/Ports/CLDC11/src/java/time/LocalTime.java b/Ports/CLDC11/src/java/time/LocalTime.java new file mode 100644 index 0000000000..da67460f44 --- /dev/null +++ b/Ports/CLDC11/src/java/time/LocalTime.java @@ -0,0 +1,131 @@ +package java.time; + +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; + +public final class LocalTime implements Comparable, TemporalAccessor { + public static final LocalTime MIDNIGHT = new LocalTime(0, 0, 0, 0); + + private final int hour; + private final int minute; + private final int second; + private final int nano; + + private LocalTime(int hour, int minute, int second, int nano) { + DateTimeSupport.checkTime(hour, minute, second, nano); + this.hour = hour; + this.minute = minute; + this.second = second; + this.nano = nano; + } + + public static LocalTime now() { + return now(Clock.systemDefaultZone()); + } + + public static LocalTime now(Clock clock) { + return LocalDateTime.ofInstant(clock.instant(), clock.getZone()).toLocalTime(); + } + + public static LocalTime of(int hour, int minute) { + return new LocalTime(hour, minute, 0, 0); + } + + public static LocalTime of(int hour, int minute, int second) { + return new LocalTime(hour, minute, second, 0); + } + + public static LocalTime of(int hour, int minute, int second, int nano) { + return new LocalTime(hour, minute, second, nano); + } + + public static LocalTime ofSecondOfDay(long secondOfDay) { + int hours = (int) (secondOfDay / 3600); + int minutes = (int) ((secondOfDay % 3600) / 60); + int seconds = (int) (secondOfDay % 60); + return of(hours, minutes, seconds); + } + + public static LocalTime ofNanoOfDay(long nanoOfDay) { + long secondOfDay = nanoOfDay / DateTimeSupport.NANOS_PER_SECOND; + int nanos = (int) (nanoOfDay % DateTimeSupport.NANOS_PER_SECOND); + LocalTime base = ofSecondOfDay(secondOfDay); + return of(base.hour, base.minute, base.second, nanos); + } + + public static LocalTime parse(CharSequence text) { + return DateTimeFormatter.ISO_LOCAL_TIME.parseLocalTime(text.toString()); + } + + public static LocalTime parse(CharSequence text, DateTimeFormatter formatter) { + return formatter.parseLocalTime(text.toString()); + } + + public int getHour() { + return hour; + } + + public int getMinute() { + return minute; + } + + public int getSecond() { + return second; + } + + public int getNano() { + return nano; + } + + public int toSecondOfDay() { + return hour * 3600 + minute * 60 + second; + } + + public long toNanoOfDay() { + return toSecondOfDay() * DateTimeSupport.NANOS_PER_SECOND + nano; + } + + public LocalTime plusHours(long hoursToAdd) { + return ofNanoOfDay(DateTimeSupport.floorMod(toNanoOfDay() + hoursToAdd * 3600L * DateTimeSupport.NANOS_PER_SECOND, + DateTimeSupport.NANOS_PER_DAY)); + } + + public LocalTime plusMinutes(long minutesToAdd) { + return ofNanoOfDay(DateTimeSupport.floorMod(toNanoOfDay() + minutesToAdd * 60L * DateTimeSupport.NANOS_PER_SECOND, + DateTimeSupport.NANOS_PER_DAY)); + } + + public LocalTime plusSeconds(long secondsToAdd) { + return ofNanoOfDay(DateTimeSupport.floorMod(toNanoOfDay() + secondsToAdd * DateTimeSupport.NANOS_PER_SECOND, + DateTimeSupport.NANOS_PER_DAY)); + } + + public String format(DateTimeFormatter formatter) { + return formatter.format(this); + } + + public int compareTo(LocalTime other) { + if (hour != other.hour) { + return hour < other.hour ? -1 : 1; + } + if (minute != other.minute) { + return minute < other.minute ? -1 : 1; + } + if (second != other.second) { + return second < other.second ? -1 : 1; + } + return nano < other.nano ? -1 : nano > other.nano ? 1 : 0; + } + + public boolean equals(Object obj) { + return obj instanceof LocalTime && compareTo((LocalTime) obj) == 0; + } + + public int hashCode() { + return hour * 3600 + minute * 60 + second + nano; + } + + public String toString() { + return DateTimeFormatter.ISO_LOCAL_TIME.format(this); + } +} diff --git a/Ports/CLDC11/src/java/time/OffsetDateTime.java b/Ports/CLDC11/src/java/time/OffsetDateTime.java new file mode 100644 index 0000000000..6bd338dbda --- /dev/null +++ b/Ports/CLDC11/src/java/time/OffsetDateTime.java @@ -0,0 +1,73 @@ +package java.time; + +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; + +public final class OffsetDateTime implements Comparable, TemporalAccessor, DateTimeSupport.TemporalCarrier { + private final LocalDateTime dateTime; + private final ZoneOffset offset; + + private OffsetDateTime(LocalDateTime dateTime, ZoneOffset offset) { + this.dateTime = dateTime; + this.offset = offset; + } + + public static OffsetDateTime of(LocalDate date, LocalTime time, ZoneOffset offset) { + return new OffsetDateTime(LocalDateTime.of(date, time), offset); + } + + public static OffsetDateTime of(LocalDateTime dateTime, ZoneOffset offset) { + return new OffsetDateTime(dateTime, offset); + } + + public static OffsetDateTime ofInstant(Instant instant, ZoneId zone) { + ZoneOffset offset = DateTimeSupport.offsetFromInstant(instant, zone); + LocalDateTime local = LocalDateTime.ofInstant(instant, zone); + return new OffsetDateTime(local, offset); + } + + public static OffsetDateTime parse(CharSequence text) { + return DateTimeFormatter.ISO_OFFSET_DATE_TIME.parseOffsetDateTime(text.toString()); + } + + public static OffsetDateTime parse(CharSequence text, DateTimeFormatter formatter) { + return formatter.parseOffsetDateTime(text.toString()); + } + + public LocalDateTime toLocalDateTime() { + return dateTime; + } + + public ZoneOffset getOffset() { + return offset; + } + + public Instant toInstant() { + return dateTime.toInstant(offset); + } + + public String format(DateTimeFormatter formatter) { + return formatter.format(this); + } + + public int compareTo(OffsetDateTime other) { + int cmp = toInstant().compareTo(other.toInstant()); + return cmp != 0 ? cmp : dateTime.compareTo(other.dateTime); + } + + public boolean equals(Object obj) { + return obj instanceof OffsetDateTime && compareTo((OffsetDateTime) obj) == 0; + } + + public int hashCode() { + return dateTime.hashCode() * 31 + offset.hashCode(); + } + + public String toString() { + return DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(this); + } + + public ZoneId getZoneForFormatting() { + return offset; + } +} diff --git a/Ports/CLDC11/src/java/time/Period.java b/Ports/CLDC11/src/java/time/Period.java new file mode 100644 index 0000000000..3fab3574b7 --- /dev/null +++ b/Ports/CLDC11/src/java/time/Period.java @@ -0,0 +1,59 @@ +package java.time; + +public final class Period { + private final int years; + private final int months; + private final int days; + + private Period(int years, int months, int days) { + this.years = years; + this.months = months; + this.days = days; + } + + public static Period of(int years, int months, int days) { + return new Period(years, months, days); + } + + public static Period ofDays(int days) { + return new Period(0, 0, days); + } + + public static Period ofMonths(int months) { + return new Period(0, months, 0); + } + + public static Period ofYears(int years) { + return new Period(years, 0, 0); + } + + public int getYears() { + return years; + } + + public int getMonths() { + return months; + } + + public int getDays() { + return days; + } + + public LocalDate addTo(LocalDate date) { + return date.plusYears(years).plusMonths(months).plusDays(days); + } + + public String toString() { + StringBuffer sb = new StringBuffer("P"); + if (years != 0) { + sb.append(years).append('Y'); + } + if (months != 0) { + sb.append(months).append('M'); + } + if (days != 0 || (years == 0 && months == 0)) { + sb.append(days).append('D'); + } + return sb.toString(); + } +} diff --git a/Ports/CLDC11/src/java/time/ZoneId.java b/Ports/CLDC11/src/java/time/ZoneId.java new file mode 100644 index 0000000000..31f638e1c7 --- /dev/null +++ b/Ports/CLDC11/src/java/time/ZoneId.java @@ -0,0 +1,58 @@ +package java.time; + +import java.util.TimeZone; + +public class ZoneId { + private final String id; + + ZoneId(String id) { + this.id = id; + } + + public static ZoneId of(String zoneId) { + if (zoneId == null) { + throw new NullPointerException(); + } + if ("Z".equals(zoneId) || "UTC".equals(zoneId) || "GMT".equals(zoneId)) { + return ZoneOffset.UTC; + } + if (zoneId.startsWith("+") || zoneId.startsWith("-")) { + return ZoneOffset.of(zoneId); + } + if (zoneId.startsWith("UTC+") || zoneId.startsWith("UTC-")) { + return ZoneOffset.of(zoneId.substring(3)); + } + if (zoneId.startsWith("GMT+") || zoneId.startsWith("GMT-")) { + return ZoneOffset.of(zoneId.substring(3)); + } + return new ZoneId(zoneId); + } + + public static ZoneId systemDefault() { + return of(TimeZone.getDefault().getID()); + } + + public String getId() { + return id; + } + + TimeZone toTimeZone() { + if (this instanceof ZoneOffset) { + ZoneOffset offset = (ZoneOffset) this; + return TimeZone.getTimeZone(offset.getId().equals("Z") ? "GMT" : "GMT" + offset.getId()); + } + return TimeZone.getTimeZone(id); + } + + public boolean equals(Object obj) { + return obj instanceof ZoneId && id.equals(((ZoneId) obj).id); + } + + public int hashCode() { + return id.hashCode(); + } + + public String toString() { + return id; + } +} diff --git a/Ports/CLDC11/src/java/time/ZoneOffset.java b/Ports/CLDC11/src/java/time/ZoneOffset.java new file mode 100644 index 0000000000..c66eca48d0 --- /dev/null +++ b/Ports/CLDC11/src/java/time/ZoneOffset.java @@ -0,0 +1,92 @@ +package java.time; + +public final class ZoneOffset extends ZoneId { + public static final ZoneOffset UTC = new ZoneOffset(0); + + private final int totalSeconds; + + private ZoneOffset(int totalSeconds) { + super(buildId(totalSeconds)); + this.totalSeconds = totalSeconds; + } + + public static ZoneOffset of(String offsetId) { + if (offsetId == null) { + throw new NullPointerException(); + } + if ("Z".equals(offsetId)) { + return UTC; + } + String text = offsetId; + char sign = text.charAt(0); + if (sign != '+' && sign != '-') { + throw new IllegalArgumentException("Invalid offset: " + offsetId); + } + text = text.substring(1); + int hours; + int minutes = 0; + if (text.indexOf(':') > 0) { + String[] parts = split(text, ':'); + hours = Integer.parseInt(parts[0]); + minutes = Integer.parseInt(parts[1]); + } else if (text.length() == 2) { + hours = Integer.parseInt(text); + } else if (text.length() == 4) { + hours = Integer.parseInt(text.substring(0, 2)); + minutes = Integer.parseInt(text.substring(2)); + } else { + throw new IllegalArgumentException("Invalid offset: " + offsetId); + } + int total = hours * 3600 + minutes * 60; + if (sign == '-') { + total = -total; + } + return ofTotalSeconds(total); + } + + private static String[] split(String value, char ch) { + int pos = value.indexOf(ch); + return new String[] { value.substring(0, pos), value.substring(pos + 1) }; + } + + public static ZoneOffset ofHours(int hours) { + return ofTotalSeconds(hours * 3600); + } + + public static ZoneOffset ofHoursMinutes(int hours, int minutes) { + int sign = hours < 0 || minutes < 0 ? -1 : 1; + return ofTotalSeconds(hours * 3600 + sign * Math.abs(minutes) * 60); + } + + public static ZoneOffset ofTotalSeconds(int totalSeconds) { + if (totalSeconds == 0) { + return UTC; + } + return new ZoneOffset(totalSeconds); + } + + public int getTotalSeconds() { + return totalSeconds; + } + + private static String buildId(int totalSeconds) { + if (totalSeconds == 0) { + return "Z"; + } + int abs = Math.abs(totalSeconds); + int hours = abs / 3600; + int minutes = (abs % 3600) / 60; + StringBuffer sb = new StringBuffer(); + sb.append(totalSeconds < 0 ? '-' : '+'); + if (hours < 10) { + sb.append('0'); + } + sb.append(hours); + sb.append(':'); + if (minutes < 10) { + sb.append('0'); + } + sb.append(minutes); + return sb.toString(); + } +} diff --git a/Ports/CLDC11/src/java/time/ZonedDateTime.java b/Ports/CLDC11/src/java/time/ZonedDateTime.java new file mode 100644 index 0000000000..96b4d93fe0 --- /dev/null +++ b/Ports/CLDC11/src/java/time/ZonedDateTime.java @@ -0,0 +1,94 @@ +package java.time; + +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; + +public final class ZonedDateTime implements Comparable, TemporalAccessor, DateTimeSupport.TemporalCarrier { + private final LocalDateTime dateTime; + private final ZoneId zone; + private final ZoneOffset offset; + + private ZonedDateTime(LocalDateTime dateTime, ZoneId zone, ZoneOffset offset) { + this.dateTime = dateTime; + this.zone = zone; + this.offset = offset; + } + + public static ZonedDateTime now() { + return now(Clock.systemDefaultZone()); + } + + public static ZonedDateTime now(Clock clock) { + return ofInstant(clock.instant(), clock.getZone()); + } + + public static ZonedDateTime of(LocalDate date, LocalTime time, ZoneId zone) { + LocalDateTime ldt = LocalDateTime.of(date, time); + Instant instant = ldt.toInstant(DateTimeSupport.offsetFromInstant(ldt.toInstant(ZoneOffset.UTC), zone)); + return ofInstant(instant, zone); + } + + public static ZonedDateTime of(LocalDateTime dateTime, ZoneId zone) { + Instant instant = dateTime.toInstant(DateTimeSupport.offsetFromInstant(dateTime.toInstant(ZoneOffset.UTC), zone)); + return ofInstant(instant, zone); + } + + public static ZonedDateTime ofInstant(Instant instant, ZoneId zone) { + ZoneOffset offset = DateTimeSupport.offsetFromInstant(instant, zone); + LocalDateTime local = LocalDateTime.ofInstant(instant, zone); + return new ZonedDateTime(local, zone, offset); + } + + public static ZonedDateTime parse(CharSequence text) { + return DateTimeFormatter.ISO_ZONED_DATE_TIME.parseZonedDateTime(text.toString()); + } + + public static ZonedDateTime parse(CharSequence text, DateTimeFormatter formatter) { + return formatter.parseZonedDateTime(text.toString()); + } + + public LocalDateTime toLocalDateTime() { + return dateTime; + } + + public ZoneId getZone() { + return zone; + } + + public ZoneOffset getOffset() { + return offset; + } + + public Instant toInstant() { + return dateTime.toInstant(offset); + } + + public String format(DateTimeFormatter formatter) { + return formatter.format(this); + } + + public int compareTo(ZonedDateTime other) { + int cmp = toInstant().compareTo(other.toInstant()); + if (cmp != 0) { + return cmp; + } + cmp = dateTime.compareTo(other.dateTime); + return cmp != 0 ? cmp : zone.getId().compareTo(other.zone.getId()); + } + + public boolean equals(Object obj) { + return obj instanceof ZonedDateTime && compareTo((ZonedDateTime) obj) == 0; + } + + public int hashCode() { + return dateTime.hashCode() * 31 + zone.hashCode(); + } + + public String toString() { + return DateTimeFormatter.ISO_ZONED_DATE_TIME.format(this); + } + + public ZoneId getZoneForFormatting() { + return zone; + } +} diff --git a/Ports/CLDC11/src/java/time/format/DateTimeFormatter.java b/Ports/CLDC11/src/java/time/format/DateTimeFormatter.java new file mode 100644 index 0000000000..a5d552c815 --- /dev/null +++ b/Ports/CLDC11/src/java/time/format/DateTimeFormatter.java @@ -0,0 +1,246 @@ +package java.time.format; + +import java.time.DateTimeSupport; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.temporal.TemporalAccessor; +import java.util.Locale; + +public final class DateTimeFormatter { + private static final int TYPE_PATTERN = 0; + private static final int TYPE_ISO_LOCAL_DATE = 1; + private static final int TYPE_ISO_LOCAL_TIME = 2; + private static final int TYPE_ISO_LOCAL_DATE_TIME = 3; + private static final int TYPE_ISO_OFFSET_DATE_TIME = 4; + private static final int TYPE_ISO_ZONED_DATE_TIME = 5; + private static final int TYPE_ISO_INSTANT = 6; + + public static final DateTimeFormatter ISO_LOCAL_DATE = new DateTimeFormatter(TYPE_ISO_LOCAL_DATE, null, null); + public static final DateTimeFormatter ISO_LOCAL_TIME = new DateTimeFormatter(TYPE_ISO_LOCAL_TIME, null, null); + public static final DateTimeFormatter ISO_LOCAL_DATE_TIME = new DateTimeFormatter(TYPE_ISO_LOCAL_DATE_TIME, null, null); + public static final DateTimeFormatter ISO_OFFSET_DATE_TIME = new DateTimeFormatter(TYPE_ISO_OFFSET_DATE_TIME, null, null); + public static final DateTimeFormatter ISO_ZONED_DATE_TIME = new DateTimeFormatter(TYPE_ISO_ZONED_DATE_TIME, null, null); + public static final DateTimeFormatter ISO_INSTANT = new DateTimeFormatter(TYPE_ISO_INSTANT, null, null); + + private final int type; + private final String pattern; + private final Locale locale; + + private DateTimeFormatter(int type, String pattern, Locale locale) { + this.type = type; + this.pattern = pattern; + this.locale = locale; + } + + public static DateTimeFormatter ofPattern(String pattern) { + return new DateTimeFormatter(TYPE_PATTERN, pattern, null); + } + + public static DateTimeFormatter ofPattern(String pattern, Locale locale) { + return new DateTimeFormatter(TYPE_PATTERN, pattern, locale); + } + + public String format(TemporalAccessor temporal) { + if (temporal == null) { + throw new NullPointerException(); + } + switch (type) { + case TYPE_PATTERN: + return formatPattern(temporal); + case TYPE_ISO_LOCAL_DATE: + return formatLocalDate((LocalDate) temporal); + case TYPE_ISO_LOCAL_TIME: + return formatLocalTime((LocalTime) temporal); + case TYPE_ISO_LOCAL_DATE_TIME: + return formatLocalDate((LocalDateTime) temporal) + "T" + formatLocalTime(((LocalDateTime) temporal).toLocalTime()); + case TYPE_ISO_OFFSET_DATE_TIME: + return formatLocalDateTime(((OffsetDateTime) temporal).toLocalDateTime()) + formatOffset(((OffsetDateTime) temporal).getOffset()); + case TYPE_ISO_ZONED_DATE_TIME: + ZonedDateTime zdt = (ZonedDateTime) temporal; + return formatLocalDateTime(zdt.toLocalDateTime()) + formatOffset(zdt.getOffset()) + "[" + zdt.getZone().getId() + "]"; + case TYPE_ISO_INSTANT: + return formatInstant((Instant) temporal); + default: + throw new IllegalStateException(); + } + } + + public TemporalAccessor parse(CharSequence text) { + switch (type) { + case TYPE_ISO_LOCAL_DATE: + return parseLocalDate(text.toString()); + case TYPE_ISO_LOCAL_TIME: + return parseLocalTime(text.toString()); + case TYPE_ISO_LOCAL_DATE_TIME: + return parseLocalDateTime(text.toString()); + case TYPE_ISO_OFFSET_DATE_TIME: + return parseOffsetDateTime(text.toString()); + case TYPE_ISO_ZONED_DATE_TIME: + return parseZonedDateTime(text.toString()); + case TYPE_ISO_INSTANT: + return parseInstant(text.toString()); + case TYPE_PATTERN: + return parseLocalDateTime(text.toString()); + default: + throw new IllegalStateException(); + } + } + + public LocalDate parseLocalDate(String text) { + if (type == TYPE_PATTERN) { + DateTimeSupport.ParsedPatternResult parsed = DateTimeSupport.parsePattern(text, pattern, ZoneOffset.UTC, locale); + return LocalDateTime.ofInstant(parsed.instant, ZoneOffset.UTC).toLocalDate(); + } + if (type != TYPE_ISO_LOCAL_DATE) { + throw new DateTimeParseException("Formatter does not produce LocalDate", text, 0); + } + return LocalDate.of(parseInt(text, 0, 4), parseInt(text, 5, 7), parseInt(text, 8, 10)); + } + + public LocalTime parseLocalTime(String text) { + if (type == TYPE_PATTERN) { + DateTimeSupport.ParsedPatternResult parsed = DateTimeSupport.parsePattern(text, pattern, ZoneOffset.UTC, locale); + return LocalDateTime.ofInstant(parsed.instant, ZoneOffset.UTC).toLocalTime(); + } + if (type != TYPE_ISO_LOCAL_TIME) { + throw new DateTimeParseException("Formatter does not produce LocalTime", text, 0); + } + int hour = parseInt(text, 0, 2); + int minute = parseInt(text, 3, 5); + int second = text.length() >= 8 ? parseInt(text, 6, 8) : 0; + int nano = 0; + int dot = text.indexOf('.'); + if (dot > 0) { + String frac = text.substring(dot + 1); + while (frac.length() < 9) { + frac += "0"; + } + if (frac.length() > 9) { + frac = frac.substring(0, 9); + } + nano = Integer.parseInt(frac); + } + return LocalTime.of(hour, minute, second, nano); + } + + public LocalDateTime parseLocalDateTime(String text) { + if (type == TYPE_PATTERN) { + DateTimeSupport.ParsedPatternResult parsed = DateTimeSupport.parsePattern(text, pattern, ZoneOffset.UTC, locale); + return LocalDateTime.ofInstant(parsed.instant, ZoneOffset.UTC); + } + if (type != TYPE_ISO_LOCAL_DATE_TIME) { + throw new DateTimeParseException("Formatter does not produce LocalDateTime", text, 0); + } + int t = text.indexOf('T'); + return LocalDateTime.of(parseLocalDate(text.substring(0, t)), parseLocalTime(text.substring(t + 1))); + } + + public OffsetDateTime parseOffsetDateTime(String text) { + if (type == TYPE_PATTERN) { + DateTimeSupport.ParsedPatternResult parsed = DateTimeSupport.parsePattern(text, pattern, ZoneOffset.UTC, locale); + return OffsetDateTime.ofInstant(parsed.instant, parsed.zone); + } + if (type != TYPE_ISO_OFFSET_DATE_TIME) { + throw new DateTimeParseException("Formatter does not produce OffsetDateTime", text, 0); + } + int idx = Math.max(text.lastIndexOf('+'), text.lastIndexOf('-')); + if (text.endsWith("Z")) { + idx = text.length() - 1; + } + LocalDateTime ldt = parseLocalDateTime(text.substring(0, idx)); + ZoneOffset offset = text.endsWith("Z") ? ZoneOffset.UTC : ZoneOffset.of(text.substring(idx)); + return OffsetDateTime.of(ldt, offset); + } + + public ZonedDateTime parseZonedDateTime(String text) { + if (type == TYPE_PATTERN) { + DateTimeSupport.ParsedPatternResult parsed = DateTimeSupport.parsePattern(text, pattern, ZoneId.systemDefault(), locale); + return ZonedDateTime.ofInstant(parsed.instant, parsed.zone); + } + if (type != TYPE_ISO_ZONED_DATE_TIME) { + throw new DateTimeParseException("Formatter does not produce ZonedDateTime", text, 0); + } + int zoneStart = text.indexOf('['); + int offsetIdx = Math.max(text.lastIndexOf('+'), text.lastIndexOf('-')); + if (text.indexOf('Z') > 0 && (offsetIdx < 0 || text.indexOf('Z') < zoneStart)) { + offsetIdx = text.indexOf('Z'); + } + LocalDateTime ldt = parseLocalDateTime(text.substring(0, offsetIdx)); + ZoneOffset offset = text.charAt(offsetIdx) == 'Z' ? ZoneOffset.UTC : ZoneOffset.of(text.substring(offsetIdx, zoneStart)); + ZoneId zone = ZoneId.of(text.substring(zoneStart + 1, text.length() - 1)); + return ZonedDateTime.ofInstant(ldt.toInstant(offset), zone); + } + + public Instant parseInstant(String text) { + if (type != TYPE_ISO_INSTANT) { + throw new DateTimeParseException("Formatter does not produce Instant", text, 0); + } + OffsetDateTime odt = ISO_OFFSET_DATE_TIME.parseOffsetDateTime(text.endsWith("Z") + ? text.substring(0, text.length() - 1) + "+00:00" + : text); + return odt.toInstant(); + } + + private String formatPattern(TemporalAccessor temporal) { + if (!(temporal instanceof DateTimeSupport.TemporalCarrier)) { + throw new IllegalArgumentException("Unsupported temporal type"); + } + return DateTimeSupport.formatPattern(pattern, (DateTimeSupport.TemporalCarrier) temporal, locale); + } + + private static int parseInt(String text, int start, int end) { + return Integer.parseInt(text.substring(start, end)); + } + + private static String pad(int value, int length) { + String s = String.valueOf(Math.abs(value)); + StringBuffer out = new StringBuffer(); + if (value < 0) { + out.append('-'); + } + for (int i = s.length(); i < length; i++) { + out.append('0'); + } + out.append(s); + return out.toString(); + } + + private static String formatLocalDate(LocalDate date) { + return pad(date.getYear(), 4) + "-" + pad(date.getMonthValue(), 2) + "-" + pad(date.getDayOfMonth(), 2); + } + + private static String formatLocalTime(LocalTime time) { + String base = pad(time.getHour(), 2) + ":" + pad(time.getMinute(), 2) + ":" + pad(time.getSecond(), 2); + if (time.getNano() == 0) { + return base; + } + String frac = String.valueOf(1000000000L + time.getNano()).substring(1); + while (frac.endsWith("0")) { + frac = frac.substring(0, frac.length() - 1); + } + return base + "." + frac; + } + + private static String formatLocalDate(LocalDateTime dateTime) { + return formatLocalDate(dateTime.toLocalDate()); + } + + private static String formatLocalDateTime(LocalDateTime dateTime) { + return formatLocalDate(dateTime.toLocalDate()) + "T" + formatLocalTime(dateTime.toLocalTime()); + } + + private static String formatOffset(ZoneOffset offset) { + return offset.getId(); + } + + private static String formatInstant(Instant instant) { + LocalDateTime dateTime = LocalDateTime.ofInstant(instant, ZoneOffset.UTC); + return formatLocalDateTime(dateTime) + "Z"; + } +} diff --git a/Ports/CLDC11/src/java/time/format/DateTimeParseException.java b/Ports/CLDC11/src/java/time/format/DateTimeParseException.java new file mode 100644 index 0000000000..e9779065a9 --- /dev/null +++ b/Ports/CLDC11/src/java/time/format/DateTimeParseException.java @@ -0,0 +1,20 @@ +package java.time.format; + +public class DateTimeParseException extends RuntimeException { + private final String parsedData; + private final int errorIndex; + + public DateTimeParseException(String message, CharSequence parsedData, int errorIndex) { + super(message); + this.parsedData = parsedData == null ? null : parsedData.toString(); + this.errorIndex = errorIndex; + } + + public String getParsedString() { + return parsedData; + } + + public int getErrorIndex() { + return errorIndex; + } +} diff --git a/Ports/CLDC11/src/java/time/temporal/TemporalAccessor.java b/Ports/CLDC11/src/java/time/temporal/TemporalAccessor.java new file mode 100644 index 0000000000..93d35a217e --- /dev/null +++ b/Ports/CLDC11/src/java/time/temporal/TemporalAccessor.java @@ -0,0 +1,4 @@ +package java.time.temporal; + +public interface TemporalAccessor { +} diff --git a/Ports/CLDC11/src/java/util/TimeZone.java b/Ports/CLDC11/src/java/util/TimeZone.java index e7ed001c59..7fda3adcbe 100644 --- a/Ports/CLDC11/src/java/util/TimeZone.java +++ b/Ports/CLDC11/src/java/util/TimeZone.java @@ -50,6 +50,9 @@ public static java.util.TimeZone getDefault(){ return null; //TODO codavaj!! } + public static void setDefault(TimeZone timezone) { + } + /** * Gets the ID of this time zone. */ diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java index de6c714c4a..94e52c6e79 100644 --- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java @@ -75,6 +75,7 @@ public final class Cn1ssDeviceRunner extends DeviceRunner { new InPlaceEditViewTest(), new BytecodeTranslatorRegressionTest(), new StreamApiTest(), + new TimeApiTest(), new Java17Tests(), new BackgroundThreadUiAccessTest(), new VPNDetectionAPITest(), diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/TimeApiTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/TimeApiTest.java new file mode 100644 index 0000000000..57b0310aa0 --- /dev/null +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/TimeApiTest.java @@ -0,0 +1,69 @@ +package com.codenameone.examples.hellocodenameone.tests; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.Period; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Locale; + +public class TimeApiTest extends BaseTest { + private String zonedString(ZonedDateTime value) { + return DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX").format(OffsetDateTime.of(value.toLocalDateTime(), value.getOffset())) + + "[" + value.getZone().getId() + "]"; + } + + @Override + public boolean shouldTakeScreenshot() { + return false; + } + + @Override + public boolean runTest() { + try { + assertEqual("2000-02-29", LocalDate.of(2000, 2, 29).toString()); + assertEqual("1900-03-01", LocalDate.of(1900, 2, 28).plusDays(1).toString()); + assertEqual("2100-03-01", LocalDate.of(2100, 2, 28).plusDays(1).toString()); + assertEqual("2020-02-29", LocalDate.of(2020, 1, 31).plusMonths(1).toString()); + + Clock fixedClock = Clock.fixed(Instant.parse("2020-06-01T10:15:30Z"), ZoneId.of("UTC")); + assertEqual("2020-06-01T10:15:30", LocalDateTime.now(fixedClock).toString()); + + ZonedDateTime beforeGap = ZonedDateTime.ofInstant(Instant.parse("2020-03-08T06:30:00Z"), ZoneId.of("America/New_York")); + ZonedDateTime afterGap = ZonedDateTime.ofInstant(Instant.parse("2020-03-08T07:30:00Z"), ZoneId.of("America/New_York")); + ZonedDateTime overlapEarly = ZonedDateTime.ofInstant(Instant.parse("2020-11-01T05:30:00Z"), ZoneId.of("America/New_York")); + ZonedDateTime overlapLate = ZonedDateTime.ofInstant(Instant.parse("2020-11-01T06:30:00Z"), ZoneId.of("America/New_York")); + + assertEqual("2020-03-08T01:30:00-05:00[America/New_York]", zonedString(beforeGap)); + assertEqual("2020-03-08T03:30:00-04:00[America/New_York]", zonedString(afterGap)); + assertEqual("2020-11-01T01:30:00-04:00[America/New_York]", zonedString(overlapEarly)); + assertEqual("2020-11-01T01:30:00-05:00[America/New_York]", zonedString(overlapLate)); + + OffsetDateTime berlin = OffsetDateTime.ofInstant(Instant.parse("2020-06-01T10:15:30Z"), ZoneId.of("Europe/Berlin")); + assertEqual("2020-06-01 12:15 +02:00", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm XXX").format(berlin)); + + LocalDateTime parsed = LocalDateTime.parse("2020-02-29 23:45:17", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + assertEqual("2020-02-29T23:45:17", parsed.toString()); + + String localized = DateTimeFormatter.ofPattern("EEE MMM dd yyyy HH:mm", new Locale("en", "US")) + .format(LocalDateTime.of(2020, 2, 29, 23, 45)); + assertEqual("Sat Feb 29 2020 23:45", localized); + + assertEqual(95061L, Duration.ofMillis(90061).plus(Duration.ofSeconds(5)).toMillis()); + Period period = Period.of(1, 1, 1); + assertEqual("2020-03-01", LocalDate.of(2019, 1, 31).plusYears(period.getYears()).plusMonths(period.getMonths()).plusDays(period.getDays()).toString()); + assertEqual("00:00:01", LocalTime.of(23, 59, 59).plusSeconds(2).toString()); + } catch (Throwable t) { + fail("Time API test failed: " + t); + return false; + } + done(); + return true; + } +} diff --git a/vm/ByteCodeTranslator/src/nativeMethods.m b/vm/ByteCodeTranslator/src/nativeMethods.m index 02049d3811..bf45d10a0e 100644 --- a/vm/ByteCodeTranslator/src/nativeMethods.m +++ b/vm/ByteCodeTranslator/src/nativeMethods.m @@ -3,6 +3,9 @@ #include #include #include +#include +#include +#include #ifndef MAX #define MAX(a,b) ((a) > (b) ? (a) : (b)) @@ -1630,6 +1633,236 @@ JAVA_OBJECT java_util_Locale_getOSLanguage___R_java_lang_String(CODENAME_ONE_THR #endif } +#if !defined(__APPLE__) || !defined(__OBJC__) +static char* cn1_strdup(const char* value) { + if (value == NULL) { + return NULL; + } + size_t len = strlen(value); + char* out = (char*)malloc(len + 1); + if (out != NULL) { + memcpy(out, value, len + 1); + } + return out; +} + +static void cn1_with_timezone(const char* zoneId, void (*func)(void*), void* ctx) { + char* original = cn1_strdup(getenv("TZ")); + if (zoneId != NULL && strlen(zoneId) > 0) { + setenv("TZ", zoneId, 1); + } else { + unsetenv("TZ"); + } + tzset(); + func(ctx); + if (original != NULL) { + setenv("TZ", original, 1); + free(original); + } else { + unsetenv("TZ"); + } + tzset(); +} + +typedef struct { + int year; + int month; + int day; + int millis; + int result; +} cn1_timezone_offset_ctx; + +static void cn1_compute_timezone_offset(void* data) { + cn1_timezone_offset_ctx* ctx = (cn1_timezone_offset_ctx*)data; + struct tm tmv; + memset(&tmv, 0, sizeof(tmv)); + tmv.tm_year = ctx->year - 1900; + tmv.tm_mon = ctx->month - 1; + tmv.tm_mday = ctx->day; + tmv.tm_hour = ctx->millis / 3600000; + tmv.tm_min = (ctx->millis / 60000) % 60; + tmv.tm_sec = (ctx->millis / 1000) % 60; + tmv.tm_isdst = -1; + time_t epoch = mktime(&tmv); + struct tm resolved; + localtime_r(&epoch, &resolved); +#ifdef __USE_MISC + ctx->result = (int)resolved.tm_gmtoff * 1000; +#else + ctx->result = 0; +#endif +} + +typedef struct { + long long millis; + int result; +} cn1_timezone_dst_ctx; + +static void cn1_compute_timezone_dst(void* data) { + cn1_timezone_dst_ctx* ctx = (cn1_timezone_dst_ctx*)data; + time_t epoch = (time_t)(ctx->millis / 1000LL); + struct tm resolved; + localtime_r(&epoch, &resolved); + ctx->result = resolved.tm_isdst > 0 ? JAVA_TRUE : JAVA_FALSE; +} + +typedef struct { + int januaryOffset; + int julyOffset; +} cn1_timezone_raw_ctx; + +static void cn1_compute_timezone_raw(void* data) { + cn1_timezone_raw_ctx* ctx = (cn1_timezone_raw_ctx*)data; + time_t now = time(NULL); + struct tm sample; + localtime_r(&now, &sample); + sample.tm_year = 124; + sample.tm_mon = 0; + sample.tm_mday = 1; + sample.tm_hour = 12; + sample.tm_min = 0; + sample.tm_sec = 0; + sample.tm_isdst = -1; + time_t january = mktime(&sample); + localtime_r(&january, &sample); +#ifdef __USE_MISC + ctx->januaryOffset = (int)sample.tm_gmtoff * 1000; +#else + ctx->januaryOffset = 0; +#endif + sample.tm_year = 124; + sample.tm_mon = 6; + sample.tm_mday = 1; + sample.tm_hour = 12; + sample.tm_min = 0; + sample.tm_sec = 0; + sample.tm_isdst = -1; + time_t july = mktime(&sample); + localtime_r(&july, &sample); +#ifdef __USE_MISC + ctx->julyOffset = (int)sample.tm_gmtoff * 1000; +#else + ctx->julyOffset = 0; +#endif +} +#endif + +JAVA_OBJECT java_util_TimeZone_getTimezoneId___R_java_lang_String(CODENAME_ONE_THREAD_STATE) { +#if defined(__APPLE__) && defined(__OBJC__) + NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; + NSString* name = [[NSTimeZone defaultTimeZone] name]; + JAVA_OBJECT out = fromNSString(threadStateData, name); + [pool release]; + return out; +#else + time_t now = time(NULL); + struct tm localTm; + localtime_r(&now, &localTm); +#ifdef __USE_MISC + if (localTm.tm_zone != NULL) { + return newStringFromCString(threadStateData, localTm.tm_zone); + } +#endif + const char* tz = getenv("TZ"); + return newStringFromCString(threadStateData, tz == NULL ? "GMT" : tz); +#endif +} + +JAVA_INT java_util_TimeZone_getTimezoneOffset___java_lang_String_int_int_int_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT name, JAVA_INT year, JAVA_INT month, JAVA_INT day, JAVA_INT timeOfDayMillis) { +#if defined(__APPLE__) && defined(__OBJC__) + NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; + NSString* n = toNSString(threadStateData, name); + NSTimeZone* tzone = [NSTimeZone timeZoneWithName:n]; + NSCalendar* cal = [[[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian] autorelease]; + [cal setTimeZone:tzone]; + NSDateComponents* comps = [[[NSDateComponents alloc] init] autorelease]; + [comps setYear:year]; + [comps setMonth:month]; + [comps setDay:day]; + [comps setHour:timeOfDayMillis / 3600000]; + [comps setMinute:(timeOfDayMillis / 60000) % 60]; + [comps setSecond:(timeOfDayMillis / 1000) % 60]; + NSDate* date = [cal dateFromComponents:comps]; + JAVA_INT out = (JAVA_INT)([tzone secondsFromGMTForDate:date] * 1000); + [pool release]; + return out; +#else + JAVA_ARRAY_CHAR chars = ((JAVA_ARRAY_CHAR)((struct obj__java_lang_String*)name)->java_lang_String_value)->data; + int len = ((struct obj__java_lang_String*)name)->java_lang_String_count; + char buffer[256]; + int copyLen = len < 255 ? len : 255; + int i; + for (i = 0; i < copyLen; i++) { + buffer[i] = (char)chars[i]; + } + buffer[copyLen] = 0; + cn1_timezone_offset_ctx ctx; + ctx.year = year; + ctx.month = month; + ctx.day = day; + ctx.millis = timeOfDayMillis; + ctx.result = 0; + cn1_with_timezone(buffer, cn1_compute_timezone_offset, &ctx); + return ctx.result; +#endif +} + +JAVA_INT java_util_TimeZone_getTimezoneRawOffset___java_lang_String_R_int(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT name) { +#if defined(__APPLE__) && defined(__OBJC__) + NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; + NSString* n = toNSString(threadStateData, name); + NSTimeZone* tzone = [NSTimeZone timeZoneWithName:n]; + JAVA_INT result = (JAVA_INT)([tzone secondsFromGMT] * 1000); + if ([tzone isDaylightSavingTime]) { + result -= (JAVA_INT)([tzone daylightSavingTimeOffset] * 1000); + } + [pool release]; + return result; +#else + JAVA_ARRAY_CHAR chars = ((JAVA_ARRAY_CHAR)((struct obj__java_lang_String*)name)->java_lang_String_value)->data; + int len = ((struct obj__java_lang_String*)name)->java_lang_String_count; + char buffer[256]; + int copyLen = len < 255 ? len : 255; + int i; + for (i = 0; i < copyLen; i++) { + buffer[i] = (char)chars[i]; + } + buffer[copyLen] = 0; + cn1_timezone_raw_ctx ctx; + ctx.januaryOffset = 0; + ctx.julyOffset = 0; + cn1_with_timezone(buffer, cn1_compute_timezone_raw, &ctx); + return abs(ctx.januaryOffset) <= abs(ctx.julyOffset) ? ctx.januaryOffset : ctx.julyOffset; +#endif +} + +JAVA_BOOLEAN java_util_TimeZone_isTimezoneDST___java_lang_String_long_R_boolean(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT name, JAVA_LONG millis) { +#if defined(__APPLE__) && defined(__OBJC__) + NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; + NSString* n = toNSString(threadStateData, name); + NSTimeZone* tzone = [NSTimeZone timeZoneWithName:n]; + NSDate* date = [NSDate dateWithTimeIntervalSince1970:(millis / 1000)]; + JAVA_BOOLEAN out = [tzone isDaylightSavingTimeForDate:date] ? JAVA_TRUE : JAVA_FALSE; + [pool release]; + return out; +#else + JAVA_ARRAY_CHAR chars = ((JAVA_ARRAY_CHAR)((struct obj__java_lang_String*)name)->java_lang_String_value)->data; + int len = ((struct obj__java_lang_String*)name)->java_lang_String_count; + char buffer[256]; + int copyLen = len < 255 ? len : 255; + int i; + for (i = 0; i < copyLen; i++) { + buffer[i] = (char)chars[i]; + } + buffer[copyLen] = 0; + cn1_timezone_dst_ctx ctx; + ctx.millis = millis; + ctx.result = JAVA_FALSE; + cn1_with_timezone(buffer, cn1_compute_timezone_dst, &ctx); + return ctx.result; +#endif +} + /*JAVA_OBJECT java_util_Locale_getOSCountry___R_java_lang_String(CODENAME_ONE_THREAD_STATE) { }*/ diff --git a/vm/JavaAPI/src/java/time/Clock.java b/vm/JavaAPI/src/java/time/Clock.java new file mode 100644 index 0000000000..85d04e71d4 --- /dev/null +++ b/vm/JavaAPI/src/java/time/Clock.java @@ -0,0 +1,57 @@ +package java.time; + +public abstract class Clock { + public abstract ZoneId getZone(); + + public abstract Instant instant(); + + public long millis() { + return instant().toEpochMilli(); + } + + public static Clock systemUTC() { + return new SystemClock(ZoneOffset.UTC); + } + + public static Clock systemDefaultZone() { + return new SystemClock(ZoneId.systemDefault()); + } + + public static Clock fixed(Instant fixedInstant, ZoneId zone) { + return new FixedClock(fixedInstant, zone); + } + + private static final class SystemClock extends Clock { + private final ZoneId zone; + + private SystemClock(ZoneId zone) { + this.zone = zone; + } + + public ZoneId getZone() { + return zone; + } + + public Instant instant() { + return Instant.now(); + } + } + + private static final class FixedClock extends Clock { + private final Instant instant; + private final ZoneId zone; + + private FixedClock(Instant instant, ZoneId zone) { + this.instant = instant; + this.zone = zone; + } + + public ZoneId getZone() { + return zone; + } + + public Instant instant() { + return instant; + } + } +} diff --git a/vm/JavaAPI/src/java/time/DateTimeSupport.java b/vm/JavaAPI/src/java/time/DateTimeSupport.java new file mode 100644 index 0000000000..92a12e6763 --- /dev/null +++ b/vm/JavaAPI/src/java/time/DateTimeSupport.java @@ -0,0 +1,252 @@ +package java.time; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +public final class DateTimeSupport { + static final long MILLIS_PER_SECOND = 1000L; + static final long MILLIS_PER_DAY = 86400000L; + static final long SECONDS_PER_DAY = 86400L; + static final long NANOS_PER_SECOND = 1000000000L; + static final long NANOS_PER_MILLI = 1000000L; + static final long NANOS_PER_DAY = 86400000000000L; + + private static final long DAYS_0000_TO_1970 = 719528L; + + private DateTimeSupport() { + } + + public static int floorDiv(int x, int y) { + int r = x / y; + if ((x ^ y) < 0 && (r * y != x)) { + r--; + } + return r; + } + + public static long floorDiv(long x, long y) { + long r = x / y; + if ((x ^ y) < 0 && (r * y != x)) { + r--; + } + return r; + } + + public static int floorMod(int x, int y) { + return x - floorDiv(x, y) * y; + } + + public static long floorMod(long x, long y) { + return x - floorDiv(x, y) * y; + } + + public static boolean isLeapYear(int year) { + return ((year & 3) == 0) && ((year % 100) != 0 || (year % 400) == 0); + } + + public static int lengthOfMonth(int year, int month) { + switch (month) { + case 2: + return isLeapYear(year) ? 29 : 28; + case 4: + case 6: + case 9: + case 11: + return 30; + default: + return 31; + } + } + + public static long toEpochDay(int year, int month, int dayOfMonth) { + long y = year; + long m = month; + long total = 365L * y; + if (y >= 0) { + total += (y + 3) / 4 - (y + 99) / 100 + (y + 399) / 400; + } else { + total -= y / -4 - y / -100 + y / -400; + } + total += ((367 * m - 362) / 12); + total += dayOfMonth - 1; + if (m > 2) { + total--; + if (!isLeapYear(year)) { + total--; + } + } + return total - DAYS_0000_TO_1970; + } + + public static int[] epochDayToDate(long epochDay) { + long zeroDay = epochDay + DAYS_0000_TO_1970; + zeroDay -= 60; + long adjust = 0; + if (zeroDay < 0) { + long adjustCycles = (zeroDay + 1) / 146097 - 1; + adjust = adjustCycles * 400; + zeroDay += -adjustCycles * 146097; + } + long yearEst = (400 * zeroDay + 591) / 146097; + long doyEst = zeroDay - (365 * yearEst + yearEst / 4 - yearEst / 100 + yearEst / 400); + if (doyEst < 0) { + yearEst--; + doyEst = zeroDay - (365 * yearEst + yearEst / 4 - yearEst / 100 + yearEst / 400); + } + yearEst += adjust; + int marchDoy0 = (int) doyEst; + int marchMonth0 = (marchDoy0 * 5 + 2) / 153; + int month = (marchMonth0 + 2) % 12 + 1; + int day = marchDoy0 - (marchMonth0 * 306 + 5) / 10 + 1; + yearEst += marchMonth0 / 10; + return new int[] { (int) yearEst, month, day }; + } + + public static void checkDate(int year, int month, int day) { + if (month < 1 || month > 12) { + throw new IllegalArgumentException("Invalid month: " + month); + } + int maxDay = lengthOfMonth(year, month); + if (day < 1 || day > maxDay) { + throw new IllegalArgumentException("Invalid day: " + day); + } + } + + public static void checkTime(int hour, int minute, int second, int nano) { + if (hour < 0 || hour > 23) { + throw new IllegalArgumentException("Invalid hour: " + hour); + } + if (minute < 0 || minute > 59) { + throw new IllegalArgumentException("Invalid minute: " + minute); + } + if (second < 0 || second > 59) { + throw new IllegalArgumentException("Invalid second: " + second); + } + if (nano < 0 || nano >= NANOS_PER_SECOND) { + throw new IllegalArgumentException("Invalid nano: " + nano); + } + } + + public static long toEpochSecond(LocalDate date, LocalTime time, ZoneOffset offset) { + long days = date.toEpochDay(); + long secs = days * SECONDS_PER_DAY + time.toSecondOfDay(); + return secs - offset.getTotalSeconds(); + } + + public static int millisOfSecond(int nano) { + return nano / 1000000; + } + + public static Calendar newCalendar(TimeZone tz) { + Calendar out = Calendar.getInstance(tz); + return out; + } + + public static Instant instantFromCalendar(Calendar cal, int nano) { + long millis = cal.getTime().getTime(); + long epochSecond = floorDiv(millis, 1000L); + int nanoAdj = (int) (floorMod(millis, 1000L) * NANOS_PER_MILLI) + nano % 1000000; + if (nanoAdj >= NANOS_PER_SECOND) { + epochSecond++; + nanoAdj -= NANOS_PER_SECOND; + } + return Instant.ofEpochSecond(epochSecond, nanoAdj); + } + + public static Calendar calendarFromLocalDateTime(LocalDate date, LocalTime time, TimeZone tz) { + Calendar cal = newCalendar(tz); + cal.set(Calendar.YEAR, date.getYear()); + cal.set(Calendar.MONTH, date.getMonthValue() - 1); + cal.set(Calendar.DAY_OF_MONTH, date.getDayOfMonth()); + cal.set(Calendar.HOUR_OF_DAY, time.getHour()); + cal.set(Calendar.MINUTE, time.getMinute()); + cal.set(Calendar.SECOND, time.getSecond()); + cal.set(Calendar.MILLISECOND, millisOfSecond(time.getNano())); + cal.getTime(); + return cal; + } + + public static LocalDateTime localDateTimeFromInstant(Instant instant, ZoneId zone) { + Calendar cal = newCalendar(zone.toTimeZone()); + cal.setTime(new Date(instant.toEpochMilli())); + return LocalDateTime.of( + cal.get(Calendar.YEAR), + cal.get(Calendar.MONTH) + 1, + cal.get(Calendar.DAY_OF_MONTH), + cal.get(Calendar.HOUR_OF_DAY), + cal.get(Calendar.MINUTE), + cal.get(Calendar.SECOND), + cal.get(Calendar.MILLISECOND) * 1000000); + } + + public static ZoneOffset offsetFromInstant(Instant instant, ZoneId zone) { + TimeZone tz = zone.toTimeZone(); + Calendar cal = newCalendar(TimeZone.getTimeZone("GMT")); + cal.setTime(new Date(instant.toEpochMilli())); + int offsetMillis = tz.getOffset( + 1, + cal.get(Calendar.YEAR), + cal.get(Calendar.MONTH), + cal.get(Calendar.DAY_OF_MONTH), + cal.get(Calendar.DAY_OF_WEEK), + ((cal.get(Calendar.HOUR_OF_DAY) * 60 + cal.get(Calendar.MINUTE)) * 60 + cal.get(Calendar.SECOND)) * 1000 + + cal.get(Calendar.MILLISECOND)); + return ZoneOffset.ofTotalSeconds(offsetMillis / 1000); + } + + public static SimpleDateFormat newFormat(String pattern, ZoneId zone, Locale locale) { + SimpleDateFormat sdf = new SimpleDateFormat(pattern); + return sdf; + } + + public static String formatPattern(String pattern, TemporalCarrier carrier, Locale locale) { + ZoneId zone = carrier.getZoneForFormatting(); + SimpleDateFormat sdf = newFormat(pattern, zone, locale); + TimeZone original = TimeZone.getDefault(); + try { + if (zone != null) { + TimeZone.setDefault(zone.toTimeZone()); + } + return sdf.format(new Date(carrier.toInstant().toEpochMilli())); + } finally { + TimeZone.setDefault(original); + } + } + + public static ParsedPatternResult parsePattern(String text, String pattern, ZoneId defaultZone, Locale locale) { + TimeZone original = TimeZone.getDefault(); + try { + if (defaultZone != null) { + TimeZone.setDefault(defaultZone.toTimeZone()); + } + SimpleDateFormat sdf = newFormat(pattern, defaultZone, locale); + Date date = sdf.parse(text); + Instant instant = Instant.ofEpochMilli(date.getTime()); + ZoneId zone = defaultZone == null ? ZoneOffset.UTC : defaultZone; + return new ParsedPatternResult(instant, zone); + } catch (ParseException err) { + throw new java.time.format.DateTimeParseException(err.getMessage(), text, 0); + } finally { + TimeZone.setDefault(original); + } + } + + public interface TemporalCarrier { + Instant toInstant(); + ZoneId getZoneForFormatting(); + } + + public static final class ParsedPatternResult { + public final Instant instant; + public final ZoneId zone; + + ParsedPatternResult(Instant instant, ZoneId zone) { + this.instant = instant; + this.zone = zone; + } + } +} diff --git a/vm/JavaAPI/src/java/time/Duration.java b/vm/JavaAPI/src/java/time/Duration.java new file mode 100644 index 0000000000..6532a92cff --- /dev/null +++ b/vm/JavaAPI/src/java/time/Duration.java @@ -0,0 +1,91 @@ +package java.time; + +public final class Duration implements Comparable { + private final long seconds; + private final int nanos; + + private Duration(long seconds, int nanos) { + this.seconds = seconds; + this.nanos = nanos; + } + + public static Duration ofDays(long days) { + return ofSeconds(days * 86400L); + } + + public static Duration ofHours(long hours) { + return ofSeconds(hours * 3600L); + } + + public static Duration ofMinutes(long minutes) { + return ofSeconds(minutes * 60L); + } + + public static Duration ofSeconds(long seconds) { + return new Duration(seconds, 0); + } + + public static Duration ofSeconds(long seconds, long nanoAdjustment) { + long secs = seconds + DateTimeSupport.floorDiv(nanoAdjustment, DateTimeSupport.NANOS_PER_SECOND); + int nanos = (int) DateTimeSupport.floorMod(nanoAdjustment, DateTimeSupport.NANOS_PER_SECOND); + return new Duration(secs, nanos); + } + + public static Duration ofMillis(long millis) { + return ofSeconds(DateTimeSupport.floorDiv(millis, 1000L), DateTimeSupport.floorMod(millis, 1000L) * 1000000L); + } + + public long getSeconds() { + return seconds; + } + + public int getNano() { + return nanos; + } + + public long toMillis() { + return seconds * 1000L + nanos / 1000000L; + } + + public Duration plus(Duration other) { + return ofSeconds(seconds + other.seconds, nanos + other.nanos); + } + + public Duration minus(Duration other) { + return ofSeconds(seconds - other.seconds, nanos - other.nanos); + } + + public int compareTo(Duration other) { + if (seconds != other.seconds) { + return seconds < other.seconds ? -1 : 1; + } + return nanos < other.nanos ? -1 : nanos > other.nanos ? 1 : 0; + } + + public boolean equals(Object obj) { + return obj instanceof Duration && compareTo((Duration) obj) == 0; + } + + public int hashCode() { + return (int) (seconds ^ (seconds >>> 32)) + nanos * 31; + } + + public String toString() { + StringBuffer sb = new StringBuffer("PT"); + long absSeconds = Math.abs(seconds); + if (seconds < 0 && absSeconds > 0) { + sb.append('-'); + } + sb.append(absSeconds); + if (nanos != 0) { + sb.append('.'); + String frac = String.valueOf(1000000000L + nanos).substring(1); + while (frac.endsWith("0")) { + frac = frac.substring(0, frac.length() - 1); + } + sb.append(frac); + } + sb.append('S'); + return sb.toString(); + } +} diff --git a/vm/JavaAPI/src/java/time/Instant.java b/vm/JavaAPI/src/java/time/Instant.java new file mode 100644 index 0000000000..ec76b882dd --- /dev/null +++ b/vm/JavaAPI/src/java/time/Instant.java @@ -0,0 +1,93 @@ +package java.time; + +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; + +public final class Instant implements Comparable, TemporalAccessor, DateTimeSupport.TemporalCarrier { + private final long epochSecond; + private final int nano; + + private Instant(long epochSecond, int nano) { + this.epochSecond = epochSecond; + this.nano = nano; + } + + public static Instant now() { + return ofEpochMilli(System.currentTimeMillis()); + } + + public static Instant ofEpochMilli(long epochMilli) { + long secs = DateTimeSupport.floorDiv(epochMilli, 1000L); + int nanos = (int) (DateTimeSupport.floorMod(epochMilli, 1000L) * 1000000L); + return new Instant(secs, nanos); + } + + public static Instant ofEpochSecond(long epochSecond) { + return new Instant(epochSecond, 0); + } + + public static Instant ofEpochSecond(long epochSecond, long nanoAdjustment) { + long secs = epochSecond + DateTimeSupport.floorDiv(nanoAdjustment, DateTimeSupport.NANOS_PER_SECOND); + int nanos = (int) DateTimeSupport.floorMod(nanoAdjustment, DateTimeSupport.NANOS_PER_SECOND); + return new Instant(secs, nanos); + } + + public static Instant parse(CharSequence text) { + return DateTimeFormatter.ISO_INSTANT.parseInstant(text.toString()); + } + + public long getEpochSecond() { + return epochSecond; + } + + public int getNano() { + return nano; + } + + public long toEpochMilli() { + return epochSecond * 1000L + nano / 1000000L; + } + + public Instant plusSeconds(long secondsToAdd) { + return ofEpochSecond(epochSecond + secondsToAdd, nano); + } + + public Instant plusMillis(long millisToAdd) { + return ofEpochMilli(toEpochMilli() + millisToAdd); + } + + public Instant minusSeconds(long secondsToSubtract) { + return plusSeconds(-secondsToSubtract); + } + + public Instant minusMillis(long millisToSubtract) { + return plusMillis(-millisToSubtract); + } + + public int compareTo(Instant other) { + if (epochSecond != other.epochSecond) { + return epochSecond < other.epochSecond ? -1 : 1; + } + return nano < other.nano ? -1 : nano > other.nano ? 1 : 0; + } + + public boolean equals(Object obj) { + return obj instanceof Instant && compareTo((Instant) obj) == 0; + } + + public int hashCode() { + return (int) (epochSecond ^ (epochSecond >>> 32)) + nano * 51; + } + + public String toString() { + return DateTimeFormatter.ISO_INSTANT.format(this); + } + + public Instant toInstant() { + return this; + } + + public ZoneId getZoneForFormatting() { + return ZoneOffset.UTC; + } +} diff --git a/vm/JavaAPI/src/java/time/LocalDate.java b/vm/JavaAPI/src/java/time/LocalDate.java new file mode 100644 index 0000000000..b633051f3b --- /dev/null +++ b/vm/JavaAPI/src/java/time/LocalDate.java @@ -0,0 +1,135 @@ +package java.time; + +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; + +public final class LocalDate implements Comparable, TemporalAccessor { + private final int year; + private final int month; + private final int dayOfMonth; + + private LocalDate(int year, int month, int dayOfMonth) { + DateTimeSupport.checkDate(year, month, dayOfMonth); + this.year = year; + this.month = month; + this.dayOfMonth = dayOfMonth; + } + + public static LocalDate now() { + return now(Clock.systemDefaultZone()); + } + + public static LocalDate now(Clock clock) { + return LocalDateTime.ofInstant(clock.instant(), clock.getZone()).toLocalDate(); + } + + public static LocalDate of(int year, int month, int dayOfMonth) { + return new LocalDate(year, month, dayOfMonth); + } + + public static LocalDate ofEpochDay(long epochDay) { + int[] parts = DateTimeSupport.epochDayToDate(epochDay); + return of(parts[0], parts[1], parts[2]); + } + + public static LocalDate parse(CharSequence text) { + return DateTimeFormatter.ISO_LOCAL_DATE.parseLocalDate(text.toString()); + } + + public static LocalDate parse(CharSequence text, DateTimeFormatter formatter) { + return formatter.parseLocalDate(text.toString()); + } + + public int getYear() { + return year; + } + + public int getMonthValue() { + return month; + } + + public int getDayOfMonth() { + return dayOfMonth; + } + + public boolean isLeapYear() { + return DateTimeSupport.isLeapYear(year); + } + + public int lengthOfMonth() { + return DateTimeSupport.lengthOfMonth(year, month); + } + + public long toEpochDay() { + return DateTimeSupport.toEpochDay(year, month, dayOfMonth); + } + + public LocalDate plusDays(long daysToAdd) { + return ofEpochDay(toEpochDay() + daysToAdd); + } + + public LocalDate plusMonths(long monthsToAdd) { + long monthCount = year * 12L + (month - 1); + long calcMonths = monthCount + monthsToAdd; + int newYear = (int) DateTimeSupport.floorDiv(calcMonths, 12); + int newMonth = (int) DateTimeSupport.floorMod(calcMonths, 12) + 1; + int newDay = Math.min(dayOfMonth, DateTimeSupport.lengthOfMonth(newYear, newMonth)); + return of(newYear, newMonth, newDay); + } + + public LocalDate plusYears(long yearsToAdd) { + int newYear = (int) (year + yearsToAdd); + int newDay = Math.min(dayOfMonth, DateTimeSupport.lengthOfMonth(newYear, month)); + return of(newYear, month, newDay); + } + + public LocalDate minusDays(long daysToSubtract) { + return plusDays(-daysToSubtract); + } + + public LocalDateTime atTime(int hour, int minute) { + return LocalDateTime.of(this, LocalTime.of(hour, minute)); + } + + public LocalDateTime atTime(int hour, int minute, int second) { + return LocalDateTime.of(this, LocalTime.of(hour, minute, second)); + } + + public LocalDateTime atTime(int hour, int minute, int second, int nano) { + return LocalDateTime.of(this, LocalTime.of(hour, minute, second, nano)); + } + + public LocalDateTime atTime(LocalTime time) { + return LocalDateTime.of(this, time); + } + + public ZonedDateTime atStartOfDay(ZoneId zone) { + return ZonedDateTime.of(this, LocalTime.MIDNIGHT, zone); + } + + public String format(DateTimeFormatter formatter) { + return formatter.format(this); + } + + public int compareTo(LocalDate other) { + if (year != other.year) { + return year < other.year ? -1 : 1; + } + if (month != other.month) { + return month < other.month ? -1 : 1; + } + return dayOfMonth < other.dayOfMonth ? -1 : dayOfMonth > other.dayOfMonth ? 1 : 0; + } + + public boolean equals(Object obj) { + return obj instanceof LocalDate && compareTo((LocalDate) obj) == 0; + } + + public int hashCode() { + return year * 37 + month * 13 + dayOfMonth; + } + + public String toString() { + return DateTimeFormatter.ISO_LOCAL_DATE.format(this); + } +} diff --git a/vm/JavaAPI/src/java/time/LocalDateTime.java b/vm/JavaAPI/src/java/time/LocalDateTime.java new file mode 100644 index 0000000000..b63c452a2c --- /dev/null +++ b/vm/JavaAPI/src/java/time/LocalDateTime.java @@ -0,0 +1,141 @@ +package java.time; + +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; + +public final class LocalDateTime implements Comparable, TemporalAccessor, DateTimeSupport.TemporalCarrier { + private final LocalDate date; + private final LocalTime time; + + private LocalDateTime(LocalDate date, LocalTime time) { + this.date = date; + this.time = time; + } + + public static LocalDateTime now() { + return now(Clock.systemDefaultZone()); + } + + public static LocalDateTime now(Clock clock) { + return ofInstant(clock.instant(), clock.getZone()); + } + + public static LocalDateTime of(LocalDate date, LocalTime time) { + return new LocalDateTime(date, time); + } + + public static LocalDateTime of(int year, int month, int dayOfMonth, int hour, int minute) { + return of(LocalDate.of(year, month, dayOfMonth), LocalTime.of(hour, minute)); + } + + public static LocalDateTime of(int year, int month, int dayOfMonth, int hour, int minute, int second) { + return of(LocalDate.of(year, month, dayOfMonth), LocalTime.of(hour, minute, second)); + } + + public static LocalDateTime of(int year, int month, int dayOfMonth, int hour, int minute, int second, int nano) { + return of(LocalDate.of(year, month, dayOfMonth), LocalTime.of(hour, minute, second, nano)); + } + + public static LocalDateTime ofInstant(Instant instant, ZoneId zone) { + return DateTimeSupport.localDateTimeFromInstant(instant, zone); + } + + public static LocalDateTime parse(CharSequence text) { + return DateTimeFormatter.ISO_LOCAL_DATE_TIME.parseLocalDateTime(text.toString()); + } + + public static LocalDateTime parse(CharSequence text, DateTimeFormatter formatter) { + return formatter.parseLocalDateTime(text.toString()); + } + + public LocalDate toLocalDate() { + return date; + } + + public LocalTime toLocalTime() { + return time; + } + + public int getYear() { + return date.getYear(); + } + + public int getMonthValue() { + return date.getMonthValue(); + } + + public int getDayOfMonth() { + return date.getDayOfMonth(); + } + + public int getHour() { + return time.getHour(); + } + + public int getMinute() { + return time.getMinute(); + } + + public int getSecond() { + return time.getSecond(); + } + + public int getNano() { + return time.getNano(); + } + + public LocalDateTime plusDays(long days) { + return of(date.plusDays(days), time); + } + + public LocalDateTime plusHours(long hours) { + long totalNanos = time.toNanoOfDay() + hours * 3600L * DateTimeSupport.NANOS_PER_SECOND; + long dayAdjust = DateTimeSupport.floorDiv(totalNanos, DateTimeSupport.NANOS_PER_DAY); + long nanoOfDay = DateTimeSupport.floorMod(totalNanos, DateTimeSupport.NANOS_PER_DAY); + return of(date.plusDays(dayAdjust), LocalTime.ofNanoOfDay(nanoOfDay)); + } + + public LocalDateTime plusMinutes(long minutes) { + return plusHours(minutes / 60).plusSeconds((minutes % 60) * 60); + } + + public LocalDateTime plusSeconds(long seconds) { + long totalNanos = time.toNanoOfDay() + seconds * DateTimeSupport.NANOS_PER_SECOND; + long dayAdjust = DateTimeSupport.floorDiv(totalNanos, DateTimeSupport.NANOS_PER_DAY); + long nanoOfDay = DateTimeSupport.floorMod(totalNanos, DateTimeSupport.NANOS_PER_DAY); + return of(date.plusDays(dayAdjust), LocalTime.ofNanoOfDay(nanoOfDay)); + } + + public Instant toInstant(ZoneOffset offset) { + return Instant.ofEpochSecond(DateTimeSupport.toEpochSecond(date, time, offset), time.getNano()); + } + + public String format(DateTimeFormatter formatter) { + return formatter.format(this); + } + + public int compareTo(LocalDateTime other) { + int cmp = date.compareTo(other.date); + return cmp != 0 ? cmp : time.compareTo(other.time); + } + + public boolean equals(Object obj) { + return obj instanceof LocalDateTime && compareTo((LocalDateTime) obj) == 0; + } + + public int hashCode() { + return date.hashCode() * 31 + time.hashCode(); + } + + public String toString() { + return DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(this); + } + + public Instant toInstant() { + return toInstant(ZoneOffset.UTC); + } + + public ZoneId getZoneForFormatting() { + return ZoneOffset.UTC; + } +} diff --git a/vm/JavaAPI/src/java/time/LocalTime.java b/vm/JavaAPI/src/java/time/LocalTime.java new file mode 100644 index 0000000000..da67460f44 --- /dev/null +++ b/vm/JavaAPI/src/java/time/LocalTime.java @@ -0,0 +1,131 @@ +package java.time; + +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; + +public final class LocalTime implements Comparable, TemporalAccessor { + public static final LocalTime MIDNIGHT = new LocalTime(0, 0, 0, 0); + + private final int hour; + private final int minute; + private final int second; + private final int nano; + + private LocalTime(int hour, int minute, int second, int nano) { + DateTimeSupport.checkTime(hour, minute, second, nano); + this.hour = hour; + this.minute = minute; + this.second = second; + this.nano = nano; + } + + public static LocalTime now() { + return now(Clock.systemDefaultZone()); + } + + public static LocalTime now(Clock clock) { + return LocalDateTime.ofInstant(clock.instant(), clock.getZone()).toLocalTime(); + } + + public static LocalTime of(int hour, int minute) { + return new LocalTime(hour, minute, 0, 0); + } + + public static LocalTime of(int hour, int minute, int second) { + return new LocalTime(hour, minute, second, 0); + } + + public static LocalTime of(int hour, int minute, int second, int nano) { + return new LocalTime(hour, minute, second, nano); + } + + public static LocalTime ofSecondOfDay(long secondOfDay) { + int hours = (int) (secondOfDay / 3600); + int minutes = (int) ((secondOfDay % 3600) / 60); + int seconds = (int) (secondOfDay % 60); + return of(hours, minutes, seconds); + } + + public static LocalTime ofNanoOfDay(long nanoOfDay) { + long secondOfDay = nanoOfDay / DateTimeSupport.NANOS_PER_SECOND; + int nanos = (int) (nanoOfDay % DateTimeSupport.NANOS_PER_SECOND); + LocalTime base = ofSecondOfDay(secondOfDay); + return of(base.hour, base.minute, base.second, nanos); + } + + public static LocalTime parse(CharSequence text) { + return DateTimeFormatter.ISO_LOCAL_TIME.parseLocalTime(text.toString()); + } + + public static LocalTime parse(CharSequence text, DateTimeFormatter formatter) { + return formatter.parseLocalTime(text.toString()); + } + + public int getHour() { + return hour; + } + + public int getMinute() { + return minute; + } + + public int getSecond() { + return second; + } + + public int getNano() { + return nano; + } + + public int toSecondOfDay() { + return hour * 3600 + minute * 60 + second; + } + + public long toNanoOfDay() { + return toSecondOfDay() * DateTimeSupport.NANOS_PER_SECOND + nano; + } + + public LocalTime plusHours(long hoursToAdd) { + return ofNanoOfDay(DateTimeSupport.floorMod(toNanoOfDay() + hoursToAdd * 3600L * DateTimeSupport.NANOS_PER_SECOND, + DateTimeSupport.NANOS_PER_DAY)); + } + + public LocalTime plusMinutes(long minutesToAdd) { + return ofNanoOfDay(DateTimeSupport.floorMod(toNanoOfDay() + minutesToAdd * 60L * DateTimeSupport.NANOS_PER_SECOND, + DateTimeSupport.NANOS_PER_DAY)); + } + + public LocalTime plusSeconds(long secondsToAdd) { + return ofNanoOfDay(DateTimeSupport.floorMod(toNanoOfDay() + secondsToAdd * DateTimeSupport.NANOS_PER_SECOND, + DateTimeSupport.NANOS_PER_DAY)); + } + + public String format(DateTimeFormatter formatter) { + return formatter.format(this); + } + + public int compareTo(LocalTime other) { + if (hour != other.hour) { + return hour < other.hour ? -1 : 1; + } + if (minute != other.minute) { + return minute < other.minute ? -1 : 1; + } + if (second != other.second) { + return second < other.second ? -1 : 1; + } + return nano < other.nano ? -1 : nano > other.nano ? 1 : 0; + } + + public boolean equals(Object obj) { + return obj instanceof LocalTime && compareTo((LocalTime) obj) == 0; + } + + public int hashCode() { + return hour * 3600 + minute * 60 + second + nano; + } + + public String toString() { + return DateTimeFormatter.ISO_LOCAL_TIME.format(this); + } +} diff --git a/vm/JavaAPI/src/java/time/OffsetDateTime.java b/vm/JavaAPI/src/java/time/OffsetDateTime.java new file mode 100644 index 0000000000..6bd338dbda --- /dev/null +++ b/vm/JavaAPI/src/java/time/OffsetDateTime.java @@ -0,0 +1,73 @@ +package java.time; + +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; + +public final class OffsetDateTime implements Comparable, TemporalAccessor, DateTimeSupport.TemporalCarrier { + private final LocalDateTime dateTime; + private final ZoneOffset offset; + + private OffsetDateTime(LocalDateTime dateTime, ZoneOffset offset) { + this.dateTime = dateTime; + this.offset = offset; + } + + public static OffsetDateTime of(LocalDate date, LocalTime time, ZoneOffset offset) { + return new OffsetDateTime(LocalDateTime.of(date, time), offset); + } + + public static OffsetDateTime of(LocalDateTime dateTime, ZoneOffset offset) { + return new OffsetDateTime(dateTime, offset); + } + + public static OffsetDateTime ofInstant(Instant instant, ZoneId zone) { + ZoneOffset offset = DateTimeSupport.offsetFromInstant(instant, zone); + LocalDateTime local = LocalDateTime.ofInstant(instant, zone); + return new OffsetDateTime(local, offset); + } + + public static OffsetDateTime parse(CharSequence text) { + return DateTimeFormatter.ISO_OFFSET_DATE_TIME.parseOffsetDateTime(text.toString()); + } + + public static OffsetDateTime parse(CharSequence text, DateTimeFormatter formatter) { + return formatter.parseOffsetDateTime(text.toString()); + } + + public LocalDateTime toLocalDateTime() { + return dateTime; + } + + public ZoneOffset getOffset() { + return offset; + } + + public Instant toInstant() { + return dateTime.toInstant(offset); + } + + public String format(DateTimeFormatter formatter) { + return formatter.format(this); + } + + public int compareTo(OffsetDateTime other) { + int cmp = toInstant().compareTo(other.toInstant()); + return cmp != 0 ? cmp : dateTime.compareTo(other.dateTime); + } + + public boolean equals(Object obj) { + return obj instanceof OffsetDateTime && compareTo((OffsetDateTime) obj) == 0; + } + + public int hashCode() { + return dateTime.hashCode() * 31 + offset.hashCode(); + } + + public String toString() { + return DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(this); + } + + public ZoneId getZoneForFormatting() { + return offset; + } +} diff --git a/vm/JavaAPI/src/java/time/Period.java b/vm/JavaAPI/src/java/time/Period.java new file mode 100644 index 0000000000..3fab3574b7 --- /dev/null +++ b/vm/JavaAPI/src/java/time/Period.java @@ -0,0 +1,59 @@ +package java.time; + +public final class Period { + private final int years; + private final int months; + private final int days; + + private Period(int years, int months, int days) { + this.years = years; + this.months = months; + this.days = days; + } + + public static Period of(int years, int months, int days) { + return new Period(years, months, days); + } + + public static Period ofDays(int days) { + return new Period(0, 0, days); + } + + public static Period ofMonths(int months) { + return new Period(0, months, 0); + } + + public static Period ofYears(int years) { + return new Period(years, 0, 0); + } + + public int getYears() { + return years; + } + + public int getMonths() { + return months; + } + + public int getDays() { + return days; + } + + public LocalDate addTo(LocalDate date) { + return date.plusYears(years).plusMonths(months).plusDays(days); + } + + public String toString() { + StringBuffer sb = new StringBuffer("P"); + if (years != 0) { + sb.append(years).append('Y'); + } + if (months != 0) { + sb.append(months).append('M'); + } + if (days != 0 || (years == 0 && months == 0)) { + sb.append(days).append('D'); + } + return sb.toString(); + } +} diff --git a/vm/JavaAPI/src/java/time/ZoneId.java b/vm/JavaAPI/src/java/time/ZoneId.java new file mode 100644 index 0000000000..31f638e1c7 --- /dev/null +++ b/vm/JavaAPI/src/java/time/ZoneId.java @@ -0,0 +1,58 @@ +package java.time; + +import java.util.TimeZone; + +public class ZoneId { + private final String id; + + ZoneId(String id) { + this.id = id; + } + + public static ZoneId of(String zoneId) { + if (zoneId == null) { + throw new NullPointerException(); + } + if ("Z".equals(zoneId) || "UTC".equals(zoneId) || "GMT".equals(zoneId)) { + return ZoneOffset.UTC; + } + if (zoneId.startsWith("+") || zoneId.startsWith("-")) { + return ZoneOffset.of(zoneId); + } + if (zoneId.startsWith("UTC+") || zoneId.startsWith("UTC-")) { + return ZoneOffset.of(zoneId.substring(3)); + } + if (zoneId.startsWith("GMT+") || zoneId.startsWith("GMT-")) { + return ZoneOffset.of(zoneId.substring(3)); + } + return new ZoneId(zoneId); + } + + public static ZoneId systemDefault() { + return of(TimeZone.getDefault().getID()); + } + + public String getId() { + return id; + } + + TimeZone toTimeZone() { + if (this instanceof ZoneOffset) { + ZoneOffset offset = (ZoneOffset) this; + return TimeZone.getTimeZone(offset.getId().equals("Z") ? "GMT" : "GMT" + offset.getId()); + } + return TimeZone.getTimeZone(id); + } + + public boolean equals(Object obj) { + return obj instanceof ZoneId && id.equals(((ZoneId) obj).id); + } + + public int hashCode() { + return id.hashCode(); + } + + public String toString() { + return id; + } +} diff --git a/vm/JavaAPI/src/java/time/ZoneOffset.java b/vm/JavaAPI/src/java/time/ZoneOffset.java new file mode 100644 index 0000000000..c66eca48d0 --- /dev/null +++ b/vm/JavaAPI/src/java/time/ZoneOffset.java @@ -0,0 +1,92 @@ +package java.time; + +public final class ZoneOffset extends ZoneId { + public static final ZoneOffset UTC = new ZoneOffset(0); + + private final int totalSeconds; + + private ZoneOffset(int totalSeconds) { + super(buildId(totalSeconds)); + this.totalSeconds = totalSeconds; + } + + public static ZoneOffset of(String offsetId) { + if (offsetId == null) { + throw new NullPointerException(); + } + if ("Z".equals(offsetId)) { + return UTC; + } + String text = offsetId; + char sign = text.charAt(0); + if (sign != '+' && sign != '-') { + throw new IllegalArgumentException("Invalid offset: " + offsetId); + } + text = text.substring(1); + int hours; + int minutes = 0; + if (text.indexOf(':') > 0) { + String[] parts = split(text, ':'); + hours = Integer.parseInt(parts[0]); + minutes = Integer.parseInt(parts[1]); + } else if (text.length() == 2) { + hours = Integer.parseInt(text); + } else if (text.length() == 4) { + hours = Integer.parseInt(text.substring(0, 2)); + minutes = Integer.parseInt(text.substring(2)); + } else { + throw new IllegalArgumentException("Invalid offset: " + offsetId); + } + int total = hours * 3600 + minutes * 60; + if (sign == '-') { + total = -total; + } + return ofTotalSeconds(total); + } + + private static String[] split(String value, char ch) { + int pos = value.indexOf(ch); + return new String[] { value.substring(0, pos), value.substring(pos + 1) }; + } + + public static ZoneOffset ofHours(int hours) { + return ofTotalSeconds(hours * 3600); + } + + public static ZoneOffset ofHoursMinutes(int hours, int minutes) { + int sign = hours < 0 || minutes < 0 ? -1 : 1; + return ofTotalSeconds(hours * 3600 + sign * Math.abs(minutes) * 60); + } + + public static ZoneOffset ofTotalSeconds(int totalSeconds) { + if (totalSeconds == 0) { + return UTC; + } + return new ZoneOffset(totalSeconds); + } + + public int getTotalSeconds() { + return totalSeconds; + } + + private static String buildId(int totalSeconds) { + if (totalSeconds == 0) { + return "Z"; + } + int abs = Math.abs(totalSeconds); + int hours = abs / 3600; + int minutes = (abs % 3600) / 60; + StringBuffer sb = new StringBuffer(); + sb.append(totalSeconds < 0 ? '-' : '+'); + if (hours < 10) { + sb.append('0'); + } + sb.append(hours); + sb.append(':'); + if (minutes < 10) { + sb.append('0'); + } + sb.append(minutes); + return sb.toString(); + } +} diff --git a/vm/JavaAPI/src/java/time/ZonedDateTime.java b/vm/JavaAPI/src/java/time/ZonedDateTime.java new file mode 100644 index 0000000000..96b4d93fe0 --- /dev/null +++ b/vm/JavaAPI/src/java/time/ZonedDateTime.java @@ -0,0 +1,94 @@ +package java.time; + +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; + +public final class ZonedDateTime implements Comparable, TemporalAccessor, DateTimeSupport.TemporalCarrier { + private final LocalDateTime dateTime; + private final ZoneId zone; + private final ZoneOffset offset; + + private ZonedDateTime(LocalDateTime dateTime, ZoneId zone, ZoneOffset offset) { + this.dateTime = dateTime; + this.zone = zone; + this.offset = offset; + } + + public static ZonedDateTime now() { + return now(Clock.systemDefaultZone()); + } + + public static ZonedDateTime now(Clock clock) { + return ofInstant(clock.instant(), clock.getZone()); + } + + public static ZonedDateTime of(LocalDate date, LocalTime time, ZoneId zone) { + LocalDateTime ldt = LocalDateTime.of(date, time); + Instant instant = ldt.toInstant(DateTimeSupport.offsetFromInstant(ldt.toInstant(ZoneOffset.UTC), zone)); + return ofInstant(instant, zone); + } + + public static ZonedDateTime of(LocalDateTime dateTime, ZoneId zone) { + Instant instant = dateTime.toInstant(DateTimeSupport.offsetFromInstant(dateTime.toInstant(ZoneOffset.UTC), zone)); + return ofInstant(instant, zone); + } + + public static ZonedDateTime ofInstant(Instant instant, ZoneId zone) { + ZoneOffset offset = DateTimeSupport.offsetFromInstant(instant, zone); + LocalDateTime local = LocalDateTime.ofInstant(instant, zone); + return new ZonedDateTime(local, zone, offset); + } + + public static ZonedDateTime parse(CharSequence text) { + return DateTimeFormatter.ISO_ZONED_DATE_TIME.parseZonedDateTime(text.toString()); + } + + public static ZonedDateTime parse(CharSequence text, DateTimeFormatter formatter) { + return formatter.parseZonedDateTime(text.toString()); + } + + public LocalDateTime toLocalDateTime() { + return dateTime; + } + + public ZoneId getZone() { + return zone; + } + + public ZoneOffset getOffset() { + return offset; + } + + public Instant toInstant() { + return dateTime.toInstant(offset); + } + + public String format(DateTimeFormatter formatter) { + return formatter.format(this); + } + + public int compareTo(ZonedDateTime other) { + int cmp = toInstant().compareTo(other.toInstant()); + if (cmp != 0) { + return cmp; + } + cmp = dateTime.compareTo(other.dateTime); + return cmp != 0 ? cmp : zone.getId().compareTo(other.zone.getId()); + } + + public boolean equals(Object obj) { + return obj instanceof ZonedDateTime && compareTo((ZonedDateTime) obj) == 0; + } + + public int hashCode() { + return dateTime.hashCode() * 31 + zone.hashCode(); + } + + public String toString() { + return DateTimeFormatter.ISO_ZONED_DATE_TIME.format(this); + } + + public ZoneId getZoneForFormatting() { + return zone; + } +} diff --git a/vm/JavaAPI/src/java/time/format/DateTimeFormatter.java b/vm/JavaAPI/src/java/time/format/DateTimeFormatter.java new file mode 100644 index 0000000000..a5d552c815 --- /dev/null +++ b/vm/JavaAPI/src/java/time/format/DateTimeFormatter.java @@ -0,0 +1,246 @@ +package java.time.format; + +import java.time.DateTimeSupport; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.temporal.TemporalAccessor; +import java.util.Locale; + +public final class DateTimeFormatter { + private static final int TYPE_PATTERN = 0; + private static final int TYPE_ISO_LOCAL_DATE = 1; + private static final int TYPE_ISO_LOCAL_TIME = 2; + private static final int TYPE_ISO_LOCAL_DATE_TIME = 3; + private static final int TYPE_ISO_OFFSET_DATE_TIME = 4; + private static final int TYPE_ISO_ZONED_DATE_TIME = 5; + private static final int TYPE_ISO_INSTANT = 6; + + public static final DateTimeFormatter ISO_LOCAL_DATE = new DateTimeFormatter(TYPE_ISO_LOCAL_DATE, null, null); + public static final DateTimeFormatter ISO_LOCAL_TIME = new DateTimeFormatter(TYPE_ISO_LOCAL_TIME, null, null); + public static final DateTimeFormatter ISO_LOCAL_DATE_TIME = new DateTimeFormatter(TYPE_ISO_LOCAL_DATE_TIME, null, null); + public static final DateTimeFormatter ISO_OFFSET_DATE_TIME = new DateTimeFormatter(TYPE_ISO_OFFSET_DATE_TIME, null, null); + public static final DateTimeFormatter ISO_ZONED_DATE_TIME = new DateTimeFormatter(TYPE_ISO_ZONED_DATE_TIME, null, null); + public static final DateTimeFormatter ISO_INSTANT = new DateTimeFormatter(TYPE_ISO_INSTANT, null, null); + + private final int type; + private final String pattern; + private final Locale locale; + + private DateTimeFormatter(int type, String pattern, Locale locale) { + this.type = type; + this.pattern = pattern; + this.locale = locale; + } + + public static DateTimeFormatter ofPattern(String pattern) { + return new DateTimeFormatter(TYPE_PATTERN, pattern, null); + } + + public static DateTimeFormatter ofPattern(String pattern, Locale locale) { + return new DateTimeFormatter(TYPE_PATTERN, pattern, locale); + } + + public String format(TemporalAccessor temporal) { + if (temporal == null) { + throw new NullPointerException(); + } + switch (type) { + case TYPE_PATTERN: + return formatPattern(temporal); + case TYPE_ISO_LOCAL_DATE: + return formatLocalDate((LocalDate) temporal); + case TYPE_ISO_LOCAL_TIME: + return formatLocalTime((LocalTime) temporal); + case TYPE_ISO_LOCAL_DATE_TIME: + return formatLocalDate((LocalDateTime) temporal) + "T" + formatLocalTime(((LocalDateTime) temporal).toLocalTime()); + case TYPE_ISO_OFFSET_DATE_TIME: + return formatLocalDateTime(((OffsetDateTime) temporal).toLocalDateTime()) + formatOffset(((OffsetDateTime) temporal).getOffset()); + case TYPE_ISO_ZONED_DATE_TIME: + ZonedDateTime zdt = (ZonedDateTime) temporal; + return formatLocalDateTime(zdt.toLocalDateTime()) + formatOffset(zdt.getOffset()) + "[" + zdt.getZone().getId() + "]"; + case TYPE_ISO_INSTANT: + return formatInstant((Instant) temporal); + default: + throw new IllegalStateException(); + } + } + + public TemporalAccessor parse(CharSequence text) { + switch (type) { + case TYPE_ISO_LOCAL_DATE: + return parseLocalDate(text.toString()); + case TYPE_ISO_LOCAL_TIME: + return parseLocalTime(text.toString()); + case TYPE_ISO_LOCAL_DATE_TIME: + return parseLocalDateTime(text.toString()); + case TYPE_ISO_OFFSET_DATE_TIME: + return parseOffsetDateTime(text.toString()); + case TYPE_ISO_ZONED_DATE_TIME: + return parseZonedDateTime(text.toString()); + case TYPE_ISO_INSTANT: + return parseInstant(text.toString()); + case TYPE_PATTERN: + return parseLocalDateTime(text.toString()); + default: + throw new IllegalStateException(); + } + } + + public LocalDate parseLocalDate(String text) { + if (type == TYPE_PATTERN) { + DateTimeSupport.ParsedPatternResult parsed = DateTimeSupport.parsePattern(text, pattern, ZoneOffset.UTC, locale); + return LocalDateTime.ofInstant(parsed.instant, ZoneOffset.UTC).toLocalDate(); + } + if (type != TYPE_ISO_LOCAL_DATE) { + throw new DateTimeParseException("Formatter does not produce LocalDate", text, 0); + } + return LocalDate.of(parseInt(text, 0, 4), parseInt(text, 5, 7), parseInt(text, 8, 10)); + } + + public LocalTime parseLocalTime(String text) { + if (type == TYPE_PATTERN) { + DateTimeSupport.ParsedPatternResult parsed = DateTimeSupport.parsePattern(text, pattern, ZoneOffset.UTC, locale); + return LocalDateTime.ofInstant(parsed.instant, ZoneOffset.UTC).toLocalTime(); + } + if (type != TYPE_ISO_LOCAL_TIME) { + throw new DateTimeParseException("Formatter does not produce LocalTime", text, 0); + } + int hour = parseInt(text, 0, 2); + int minute = parseInt(text, 3, 5); + int second = text.length() >= 8 ? parseInt(text, 6, 8) : 0; + int nano = 0; + int dot = text.indexOf('.'); + if (dot > 0) { + String frac = text.substring(dot + 1); + while (frac.length() < 9) { + frac += "0"; + } + if (frac.length() > 9) { + frac = frac.substring(0, 9); + } + nano = Integer.parseInt(frac); + } + return LocalTime.of(hour, minute, second, nano); + } + + public LocalDateTime parseLocalDateTime(String text) { + if (type == TYPE_PATTERN) { + DateTimeSupport.ParsedPatternResult parsed = DateTimeSupport.parsePattern(text, pattern, ZoneOffset.UTC, locale); + return LocalDateTime.ofInstant(parsed.instant, ZoneOffset.UTC); + } + if (type != TYPE_ISO_LOCAL_DATE_TIME) { + throw new DateTimeParseException("Formatter does not produce LocalDateTime", text, 0); + } + int t = text.indexOf('T'); + return LocalDateTime.of(parseLocalDate(text.substring(0, t)), parseLocalTime(text.substring(t + 1))); + } + + public OffsetDateTime parseOffsetDateTime(String text) { + if (type == TYPE_PATTERN) { + DateTimeSupport.ParsedPatternResult parsed = DateTimeSupport.parsePattern(text, pattern, ZoneOffset.UTC, locale); + return OffsetDateTime.ofInstant(parsed.instant, parsed.zone); + } + if (type != TYPE_ISO_OFFSET_DATE_TIME) { + throw new DateTimeParseException("Formatter does not produce OffsetDateTime", text, 0); + } + int idx = Math.max(text.lastIndexOf('+'), text.lastIndexOf('-')); + if (text.endsWith("Z")) { + idx = text.length() - 1; + } + LocalDateTime ldt = parseLocalDateTime(text.substring(0, idx)); + ZoneOffset offset = text.endsWith("Z") ? ZoneOffset.UTC : ZoneOffset.of(text.substring(idx)); + return OffsetDateTime.of(ldt, offset); + } + + public ZonedDateTime parseZonedDateTime(String text) { + if (type == TYPE_PATTERN) { + DateTimeSupport.ParsedPatternResult parsed = DateTimeSupport.parsePattern(text, pattern, ZoneId.systemDefault(), locale); + return ZonedDateTime.ofInstant(parsed.instant, parsed.zone); + } + if (type != TYPE_ISO_ZONED_DATE_TIME) { + throw new DateTimeParseException("Formatter does not produce ZonedDateTime", text, 0); + } + int zoneStart = text.indexOf('['); + int offsetIdx = Math.max(text.lastIndexOf('+'), text.lastIndexOf('-')); + if (text.indexOf('Z') > 0 && (offsetIdx < 0 || text.indexOf('Z') < zoneStart)) { + offsetIdx = text.indexOf('Z'); + } + LocalDateTime ldt = parseLocalDateTime(text.substring(0, offsetIdx)); + ZoneOffset offset = text.charAt(offsetIdx) == 'Z' ? ZoneOffset.UTC : ZoneOffset.of(text.substring(offsetIdx, zoneStart)); + ZoneId zone = ZoneId.of(text.substring(zoneStart + 1, text.length() - 1)); + return ZonedDateTime.ofInstant(ldt.toInstant(offset), zone); + } + + public Instant parseInstant(String text) { + if (type != TYPE_ISO_INSTANT) { + throw new DateTimeParseException("Formatter does not produce Instant", text, 0); + } + OffsetDateTime odt = ISO_OFFSET_DATE_TIME.parseOffsetDateTime(text.endsWith("Z") + ? text.substring(0, text.length() - 1) + "+00:00" + : text); + return odt.toInstant(); + } + + private String formatPattern(TemporalAccessor temporal) { + if (!(temporal instanceof DateTimeSupport.TemporalCarrier)) { + throw new IllegalArgumentException("Unsupported temporal type"); + } + return DateTimeSupport.formatPattern(pattern, (DateTimeSupport.TemporalCarrier) temporal, locale); + } + + private static int parseInt(String text, int start, int end) { + return Integer.parseInt(text.substring(start, end)); + } + + private static String pad(int value, int length) { + String s = String.valueOf(Math.abs(value)); + StringBuffer out = new StringBuffer(); + if (value < 0) { + out.append('-'); + } + for (int i = s.length(); i < length; i++) { + out.append('0'); + } + out.append(s); + return out.toString(); + } + + private static String formatLocalDate(LocalDate date) { + return pad(date.getYear(), 4) + "-" + pad(date.getMonthValue(), 2) + "-" + pad(date.getDayOfMonth(), 2); + } + + private static String formatLocalTime(LocalTime time) { + String base = pad(time.getHour(), 2) + ":" + pad(time.getMinute(), 2) + ":" + pad(time.getSecond(), 2); + if (time.getNano() == 0) { + return base; + } + String frac = String.valueOf(1000000000L + time.getNano()).substring(1); + while (frac.endsWith("0")) { + frac = frac.substring(0, frac.length() - 1); + } + return base + "." + frac; + } + + private static String formatLocalDate(LocalDateTime dateTime) { + return formatLocalDate(dateTime.toLocalDate()); + } + + private static String formatLocalDateTime(LocalDateTime dateTime) { + return formatLocalDate(dateTime.toLocalDate()) + "T" + formatLocalTime(dateTime.toLocalTime()); + } + + private static String formatOffset(ZoneOffset offset) { + return offset.getId(); + } + + private static String formatInstant(Instant instant) { + LocalDateTime dateTime = LocalDateTime.ofInstant(instant, ZoneOffset.UTC); + return formatLocalDateTime(dateTime) + "Z"; + } +} diff --git a/vm/JavaAPI/src/java/time/format/DateTimeParseException.java b/vm/JavaAPI/src/java/time/format/DateTimeParseException.java new file mode 100644 index 0000000000..e9779065a9 --- /dev/null +++ b/vm/JavaAPI/src/java/time/format/DateTimeParseException.java @@ -0,0 +1,20 @@ +package java.time.format; + +public class DateTimeParseException extends RuntimeException { + private final String parsedData; + private final int errorIndex; + + public DateTimeParseException(String message, CharSequence parsedData, int errorIndex) { + super(message); + this.parsedData = parsedData == null ? null : parsedData.toString(); + this.errorIndex = errorIndex; + } + + public String getParsedString() { + return parsedData; + } + + public int getErrorIndex() { + return errorIndex; + } +} diff --git a/vm/JavaAPI/src/java/time/temporal/TemporalAccessor.java b/vm/JavaAPI/src/java/time/temporal/TemporalAccessor.java new file mode 100644 index 0000000000..93d35a217e --- /dev/null +++ b/vm/JavaAPI/src/java/time/temporal/TemporalAccessor.java @@ -0,0 +1,4 @@ +package java.time.temporal; + +public interface TemporalAccessor { +} diff --git a/vm/JavaAPI/src/java/util/TimeZone.java b/vm/JavaAPI/src/java/util/TimeZone.java index 0a525ecebb..2b67656bce 100644 --- a/vm/JavaAPI/src/java/util/TimeZone.java +++ b/vm/JavaAPI/src/java/util/TimeZone.java @@ -128,6 +128,10 @@ public boolean useDaylightTime() { } return defaultTimeZone; } + + public static void setDefault(TimeZone timezone) { + defaultTimeZone = timezone; + } diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/TimeApiIntegrationTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/TimeApiIntegrationTest.java new file mode 100644 index 0000000000..18cbb2ceda --- /dev/null +++ b/vm/tests/src/test/java/com/codename1/tools/translator/TimeApiIntegrationTest.java @@ -0,0 +1,153 @@ +package com.codename1.tools.translator; + +import org.junit.jupiter.api.Test; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +class TimeApiIntegrationTest { + + @Test + void timeEdgeCasesMatchBetweenJavaSEAndParparVM() throws Exception { + Parser.cleanup(); + + Path sourceDir = Files.createTempDirectory("time-integration-sources"); + Path classesDir = Files.createTempDirectory("time-integration-classes"); + Path javaApiDir = Files.createTempDirectory("java-api-classes"); + + Path source = sourceDir.resolve("TimeEdgeApp.java"); + Files.write(source, loadAppSource().getBytes(StandardCharsets.UTF_8)); + + CompilerHelper.CompilerConfig config = selectCompiler(); + if (config == null) { + fail("No compatible compiler available for time integration test"); + } + + assertTrue(CompilerHelper.isJavaApiCompatible(config), + "JDK " + config.jdkVersion + " must target matching bytecode level for JavaAPI"); + + CompilerHelper.compileJavaAPI(javaApiDir, config); + + List compileArgs = new ArrayList<>(); + compileArgs.add("-source"); + compileArgs.add(config.targetVersion); + compileArgs.add("-target"); + compileArgs.add(config.targetVersion); + if (CompilerHelper.useClasspath(config)) { + compileArgs.add("-classpath"); + compileArgs.add(javaApiDir.toString()); + } else { + compileArgs.add("-bootclasspath"); + compileArgs.add(javaApiDir.toString()); + compileArgs.add("-Xlint:-options"); + } + compileArgs.add("-d"); + compileArgs.add(classesDir.toString()); + compileArgs.add(source.toString()); + + int compileResult = CompilerHelper.compile(config.jdkHome, compileArgs); + assertEquals(0, compileResult, "TimeEdgeApp should compile. " + CompilerHelper.getLastErrorLog()); + + String javaOutput = runJavaMain(config, classesDir, javaApiDir); + String javaResult = extractResultLine(javaOutput); + assertTrue(javaResult.startsWith("RESULT="), "JavaSE should produce a RESULT line. Output: " + javaOutput); + + CompilerHelper.copyDirectory(javaApiDir, classesDir); + + Path outputDir = Files.createTempDirectory("time-integration-output"); + CleanTargetIntegrationTest.runTranslator(classesDir, outputDir, "TimeEdgeApp"); + + Path distDir = outputDir.resolve("dist"); + Path cmakeLists = distDir.resolve("CMakeLists.txt"); + assertTrue(Files.exists(cmakeLists), "Translator should emit a CMake project"); + + CleanTargetIntegrationTest.replaceLibraryWithExecutableTarget(cmakeLists, "TimeEdgeApp-src"); + + Path buildDir = distDir.resolve("build"); + Files.createDirectories(buildDir); + + CleanTargetIntegrationTest.runCommand(Arrays.asList( + "cmake", + "-S", distDir.toString(), + "-B", buildDir.toString(), + "-DCMAKE_C_COMPILER=clang", + "-DCMAKE_OBJC_COMPILER=clang" + ), distDir); + + CleanTargetIntegrationTest.runCommand(Arrays.asList("cmake", "--build", buildDir.toString()), distDir); + + Path executable = buildDir.resolve("TimeEdgeApp"); + String parparOutput = CleanTargetIntegrationTest.runCommand(Arrays.asList(executable.toString()), buildDir); + String parparResult = extractResultLine(parparOutput); + assertTrue(parparResult.startsWith("RESULT="), "ParparVM execution should produce a RESULT line. Output: " + parparOutput); + + assertEquals(javaResult, parparResult, "JavaSE and ParparVM should emit identical result lines for time edge cases"); + } + + private String loadAppSource() throws Exception { + java.io.InputStream in = TimeApiIntegrationTest.class.getResourceAsStream("/com/codename1/tools/translator/TimeEdgeApp.java"); + assertNotNull(in, "TimeEdgeApp.java test resource should exist"); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) { + return reader.lines().collect(Collectors.joining("\n")) + "\n"; + } + } + + private String runJavaMain(CompilerHelper.CompilerConfig config, Path classesDir, Path javaApiDir) throws Exception { + String javaExe = config.jdkHome.resolve("bin").resolve("java").toString(); + if (System.getProperty("os.name").toLowerCase().contains("win")) { + javaExe += ".exe"; + } + + ProcessBuilder pb = new ProcessBuilder( + javaExe, + "-cp", + classesDir + System.getProperty("path.separator") + javaApiDir, + "TimeEdgeApp" + ); + pb.redirectErrorStream(true); + + Process process = pb.start(); + String output; + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { + output = reader.lines().collect(Collectors.joining("\n")); + } + + int exitCode = process.waitFor(); + assertEquals(0, exitCode, "JVM run should exit cleanly. Output: " + output); + return output; + } + + private String extractResultLine(String output) { + for (String line : output.split("\\R")) { + if (line.startsWith("RESULT=")) { + return line.trim(); + } + } + return ""; + } + + private CompilerHelper.CompilerConfig selectCompiler() { + String[] preferredTargets = {"11", "17", "21", "25", "1.8"}; + for (String target : preferredTargets) { + List configs = CompilerHelper.getAvailableCompilers(target); + for (CompilerHelper.CompilerConfig config : configs) { + if (CompilerHelper.isJavaApiCompatible(config)) { + return config; + } + } + } + return null; + } +} diff --git a/vm/tests/src/test/resources/com/codename1/tools/translator/TimeEdgeApp.java b/vm/tests/src/test/resources/com/codename1/tools/translator/TimeEdgeApp.java new file mode 100644 index 0000000000..10acbca159 --- /dev/null +++ b/vm/tests/src/test/resources/com/codename1/tools/translator/TimeEdgeApp.java @@ -0,0 +1,67 @@ +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.Period; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Locale; + +public class TimeEdgeApp { + private static String zonedString(ZonedDateTime value) { + return DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX").format(OffsetDateTime.of(value.toLocalDateTime(), value.getOffset())) + + "[" + value.getZone().getId() + "]"; + } + + private static String calculate() { + LocalDate leap2000 = LocalDate.of(2000, 2, 29); + LocalDate century1900 = LocalDate.of(1900, 2, 28).plusDays(1); + LocalDate century2100 = LocalDate.of(2100, 2, 28).plusDays(1); + LocalDate leap2004 = LocalDate.of(2004, 2, 29).plusYears(1); + LocalDate rollover = LocalDate.of(2020, 1, 31).plusMonths(1); + + Instant baseInstant = Instant.parse("2020-03-08T06:30:00Z"); + ZonedDateTime nyBeforeGap = ZonedDateTime.ofInstant(baseInstant, ZoneId.of("America/New_York")); + ZonedDateTime nyAfterGap = ZonedDateTime.ofInstant(Instant.parse("2020-03-08T07:30:00Z"), ZoneId.of("America/New_York")); + ZonedDateTime nyOverlapEarly = ZonedDateTime.ofInstant(Instant.parse("2020-11-01T05:30:00Z"), ZoneId.of("America/New_York")); + ZonedDateTime nyOverlapLate = ZonedDateTime.ofInstant(Instant.parse("2020-11-01T06:30:00Z"), ZoneId.of("America/New_York")); + ZonedDateTime berlinSummer = ZonedDateTime.ofInstant(Instant.parse("2020-06-01T10:15:30Z"), ZoneId.of("Europe/Berlin")); + + OffsetDateTime offset = OffsetDateTime.ofInstant(Instant.parse("2020-06-01T10:15:30Z"), ZoneId.of("Europe/Berlin")); + String formattedOffset = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm XXX").format(offset); + LocalDateTime parsedPattern = LocalDateTime.parse("2020-02-29 23:45:17", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + String localized = DateTimeFormatter.ofPattern("EEE MMM dd yyyy HH:mm", new Locale("en", "US")) + .format(LocalDateTime.of(2020, 2, 29, 23, 45)); + + Duration duration = Duration.ofMillis(90061).plus(Duration.ofSeconds(5)); + Period period = Period.of(1, 1, 1); + LocalDate periodTarget = LocalDate.of(2019, 1, 31).plusYears(period.getYears()).plusMonths(period.getMonths()).plusDays(period.getDays()); + + StringBuilder result = new StringBuilder(); + result.append(leap2000).append('|'); + result.append(century1900).append('|'); + result.append(century2100).append('|'); + result.append(leap2004).append('|'); + result.append(rollover).append('|'); + result.append(zonedString(nyBeforeGap)).append('|'); + result.append(zonedString(nyAfterGap)).append('|'); + result.append(zonedString(nyOverlapEarly)).append('|'); + result.append(zonedString(nyOverlapLate)).append('|'); + result.append(zonedString(berlinSummer)).append('|'); + result.append(formattedOffset).append('|'); + result.append(parsedPattern).append('|'); + result.append(localized).append('|'); + result.append(duration.toMillis()).append('|'); + result.append(periodTarget).append('|'); + result.append(LocalTime.of(23, 59, 59).plusSeconds(2)).append('|'); + result.append(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss").format(LocalDateTime.ofInstant(baseInstant, ZoneId.of("UTC")))); + return result.toString(); + } + + public static void main(String[] args) { + System.out.println("RESULT=" + calculate()); + } +} From 33b44066a30694fa9c490dfff9c6901ab15b29ab Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 20 Mar 2026 09:42:59 +0200 Subject: [PATCH 2/3] Narrowed down the build to prevent duplicate includes --- vm/ByteCodeTranslator/src/nativeMethods.m | 51 +---------------------- 1 file changed, 2 insertions(+), 49 deletions(-) diff --git a/vm/ByteCodeTranslator/src/nativeMethods.m b/vm/ByteCodeTranslator/src/nativeMethods.m index bf45d10a0e..f7c17ea215 100644 --- a/vm/ByteCodeTranslator/src/nativeMethods.m +++ b/vm/ByteCodeTranslator/src/nativeMethods.m @@ -1747,14 +1747,8 @@ static void cn1_compute_timezone_raw(void* data) { } #endif +#if !defined(__APPLE__) || !defined(__OBJC__) JAVA_OBJECT java_util_TimeZone_getTimezoneId___R_java_lang_String(CODENAME_ONE_THREAD_STATE) { -#if defined(__APPLE__) && defined(__OBJC__) - NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; - NSString* name = [[NSTimeZone defaultTimeZone] name]; - JAVA_OBJECT out = fromNSString(threadStateData, name); - [pool release]; - return out; -#else time_t now = time(NULL); struct tm localTm; localtime_r(&now, &localTm); @@ -1765,28 +1759,9 @@ JAVA_OBJECT java_util_TimeZone_getTimezoneId___R_java_lang_String(CODENAME_ONE_T #endif const char* tz = getenv("TZ"); return newStringFromCString(threadStateData, tz == NULL ? "GMT" : tz); -#endif } JAVA_INT java_util_TimeZone_getTimezoneOffset___java_lang_String_int_int_int_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT name, JAVA_INT year, JAVA_INT month, JAVA_INT day, JAVA_INT timeOfDayMillis) { -#if defined(__APPLE__) && defined(__OBJC__) - NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; - NSString* n = toNSString(threadStateData, name); - NSTimeZone* tzone = [NSTimeZone timeZoneWithName:n]; - NSCalendar* cal = [[[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian] autorelease]; - [cal setTimeZone:tzone]; - NSDateComponents* comps = [[[NSDateComponents alloc] init] autorelease]; - [comps setYear:year]; - [comps setMonth:month]; - [comps setDay:day]; - [comps setHour:timeOfDayMillis / 3600000]; - [comps setMinute:(timeOfDayMillis / 60000) % 60]; - [comps setSecond:(timeOfDayMillis / 1000) % 60]; - NSDate* date = [cal dateFromComponents:comps]; - JAVA_INT out = (JAVA_INT)([tzone secondsFromGMTForDate:date] * 1000); - [pool release]; - return out; -#else JAVA_ARRAY_CHAR chars = ((JAVA_ARRAY_CHAR)((struct obj__java_lang_String*)name)->java_lang_String_value)->data; int len = ((struct obj__java_lang_String*)name)->java_lang_String_count; char buffer[256]; @@ -1804,21 +1779,9 @@ JAVA_INT java_util_TimeZone_getTimezoneOffset___java_lang_String_int_int_int_int ctx.result = 0; cn1_with_timezone(buffer, cn1_compute_timezone_offset, &ctx); return ctx.result; -#endif } JAVA_INT java_util_TimeZone_getTimezoneRawOffset___java_lang_String_R_int(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT name) { -#if defined(__APPLE__) && defined(__OBJC__) - NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; - NSString* n = toNSString(threadStateData, name); - NSTimeZone* tzone = [NSTimeZone timeZoneWithName:n]; - JAVA_INT result = (JAVA_INT)([tzone secondsFromGMT] * 1000); - if ([tzone isDaylightSavingTime]) { - result -= (JAVA_INT)([tzone daylightSavingTimeOffset] * 1000); - } - [pool release]; - return result; -#else JAVA_ARRAY_CHAR chars = ((JAVA_ARRAY_CHAR)((struct obj__java_lang_String*)name)->java_lang_String_value)->data; int len = ((struct obj__java_lang_String*)name)->java_lang_String_count; char buffer[256]; @@ -1833,19 +1796,9 @@ JAVA_INT java_util_TimeZone_getTimezoneRawOffset___java_lang_String_R_int(CODENA ctx.julyOffset = 0; cn1_with_timezone(buffer, cn1_compute_timezone_raw, &ctx); return abs(ctx.januaryOffset) <= abs(ctx.julyOffset) ? ctx.januaryOffset : ctx.julyOffset; -#endif } JAVA_BOOLEAN java_util_TimeZone_isTimezoneDST___java_lang_String_long_R_boolean(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT name, JAVA_LONG millis) { -#if defined(__APPLE__) && defined(__OBJC__) - NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; - NSString* n = toNSString(threadStateData, name); - NSTimeZone* tzone = [NSTimeZone timeZoneWithName:n]; - NSDate* date = [NSDate dateWithTimeIntervalSince1970:(millis / 1000)]; - JAVA_BOOLEAN out = [tzone isDaylightSavingTimeForDate:date] ? JAVA_TRUE : JAVA_FALSE; - [pool release]; - return out; -#else JAVA_ARRAY_CHAR chars = ((JAVA_ARRAY_CHAR)((struct obj__java_lang_String*)name)->java_lang_String_value)->data; int len = ((struct obj__java_lang_String*)name)->java_lang_String_count; char buffer[256]; @@ -1860,8 +1813,8 @@ JAVA_BOOLEAN java_util_TimeZone_isTimezoneDST___java_lang_String_long_R_boolean( ctx.result = JAVA_FALSE; cn1_with_timezone(buffer, cn1_compute_timezone_dst, &ctx); return ctx.result; -#endif } +#endif /*JAVA_OBJECT java_util_Locale_getOSCountry___R_java_lang_String(CODENAME_ONE_THREAD_STATE) { }*/ From ac70ba578e2ce0716e94cac68304a3a93bd582c4 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:17:20 +0200 Subject: [PATCH 3/3] Fixed compilation issues --- .../CLDC11/src/java/time/DateTimeSupport.java | 17 ++--- .../tools/translator/ByteCodeTranslator.java | 16 +++-- .../codename1/tools/translator/Parser.java | 30 ++++++--- vm/ByteCodeTranslator/src/nativeMethods.m | 66 +++++++------------ vm/JavaAPI/src/java/time/DateTimeSupport.java | 17 ++--- .../CleanTargetIntegrationTest.java | 5 ++ .../translator/TimeApiIntegrationTest.java | 9 ++- .../tools/translator/TimeEdgeApp.java | 28 ++++---- 8 files changed, 95 insertions(+), 93 deletions(-) diff --git a/Ports/CLDC11/src/java/time/DateTimeSupport.java b/Ports/CLDC11/src/java/time/DateTimeSupport.java index 7d26faa10e..d6e3e5d05a 100644 --- a/Ports/CLDC11/src/java/time/DateTimeSupport.java +++ b/Ports/CLDC11/src/java/time/DateTimeSupport.java @@ -147,16 +147,13 @@ public static Calendar newCalendar(TimeZone tz) { } public static LocalDateTime localDateTimeFromInstant(Instant instant, ZoneId zone) { - Calendar cal = newCalendar(zone.toTimeZone()); - cal.setTime(new Date(instant.toEpochMilli())); - return LocalDateTime.of( - cal.get(Calendar.YEAR), - cal.get(Calendar.MONTH) + 1, - cal.get(Calendar.DAY_OF_MONTH), - cal.get(Calendar.HOUR_OF_DAY), - cal.get(Calendar.MINUTE), - cal.get(Calendar.SECOND), - cal.get(Calendar.MILLISECOND) * 1000000); + ZoneOffset offset = offsetFromInstant(instant, zone); + long localSecond = instant.getEpochSecond() + offset.getTotalSeconds(); + long epochDay = floorDiv(localSecond, SECONDS_PER_DAY); + int secondOfDay = (int) floorMod(localSecond, SECONDS_PER_DAY); + LocalDate date = LocalDate.ofEpochDay(epochDay); + LocalTime time = LocalTime.ofNanoOfDay(secondOfDay * NANOS_PER_SECOND + instant.getNano()); + return LocalDateTime.of(date, time); } public static ZoneOffset offsetFromInstant(Instant instant, ZoneId zone) { diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java index 2750bcdbe5..45e8b1d812 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java @@ -202,7 +202,9 @@ private static void handleDefaultOutput(ByteCodeTranslator b, File[] sources, Fi private static void handleCleanOutput(ByteCodeTranslator b, File[] sources, File dest, String appName) throws Exception { File root = new File(dest, "dist"); root.mkdirs(); - System.out.println("Root is: " + root.getAbsolutePath()); + if(verbose) { + System.out.println("Root is: " + root.getAbsolutePath()); + } File srcRoot = new File(root, appName + "-src"); srcRoot.mkdirs(); @@ -251,12 +253,16 @@ private static void handleCleanOutput(ByteCodeTranslator b, File[] sources, File private static void handleIosOutput(ByteCodeTranslator b, File[] sources, File dest, String appName, String appPackageName, String appDisplayName, String appVersion, String appType, String addFrameworks) throws Exception { File root = new File(dest, "dist"); root.mkdirs(); - System.out.println("Root is: " + root.getAbsolutePath()); + if(verbose) { + System.out.println("Root is: " + root.getAbsolutePath()); + } File srcRoot = new File(root, appName + "-src"); srcRoot.mkdirs(); //cleanDir(srcRoot); - System.out.println("srcRoot is: " + srcRoot.getAbsolutePath() ); + if(verbose) { + System.out.println("srcRoot is: " + srcRoot.getAbsolutePath() ); + } File imagesXcassets = new File(srcRoot, "Images.xcassets"); imagesXcassets.mkdirs(); @@ -631,7 +637,9 @@ private static void replaceInFile(File sourceFile, String... values) throws IOEx // // don't start the output file until all the processing is done // - System.out.println("Rewrite " + sourceFile + " with " + totchanges + " changes"); + if(verbose) { + System.out.println("Rewrite " + sourceFile + " with " + totchanges + " changes"); + } try(Writer fios = new OutputStreamWriter(Files.newOutputStream(sourceFile.toPath()), StandardCharsets.UTF_8)) { fios.write(str.toString()); } diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/Parser.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/Parser.java index 6fbba4d899..ab9203e555 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/Parser.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/Parser.java @@ -365,7 +365,9 @@ private static String fourChars(String s) { } public static void writeOutput(File outputDirectory) throws Exception { - System.out.println("outputDirectory is: " + outputDirectory.getAbsolutePath() ); + if(ByteCodeTranslator.verbose) { + System.out.println("outputDirectory is: " + outputDirectory.getAbsolutePath() ); + } if(ByteCodeClass.getMainClass()==null){ System.out.println("Error main class is not defined. The main class name is expected to have a public static void main(String[]) method and it is assumed to reside in the com.package.name directory"); System.exit(1); @@ -423,12 +425,16 @@ public static void writeOutput(File outputDirectory) throws Exception { // loop over methods and start eliminating the body of unused methods if (BytecodeMethod.optimizerOn) { - System.out.println("Optimizer On: Removing unused methods and classes..."); + if(ByteCodeTranslator.verbose) { + System.out.println("Optimizer On: Removing unused methods and classes..."); + } Date now = new Date(); neliminated += eliminateUnusedMethods(); Date later = new Date(); long dif = later.getTime()-now.getTime(); - System.out.println("unused Method cull removed "+neliminated+" methods in "+(dif/1000)+" seconds"); + if(ByteCodeTranslator.verbose) { + System.out.println("unused Method cull removed "+neliminated+" methods in "+(dif/1000)+" seconds"); + } } generateClassAndMethodIndexHeader(outputDirectory); @@ -459,7 +465,9 @@ private static void readNativeFiles(File outputDirectory) throws IOException { } nativeSources = new String[mFiles.length]; int size = 0; - System.out.println(mFiles.length + " native files"); + if(ByteCodeTranslator.verbose) { + System.out.println(mFiles.length + " native files"); + } for(int iter = 0 ; iter < mFiles.length ; iter++) { FileInputStream fi = new FileInputStream(mFiles[iter]); DataInputStream di = new DataInputStream(fi); @@ -470,7 +478,9 @@ private static void readNativeFiles(File outputDirectory) throws IOException { fi.close(); nativeSources[iter] = new String(dat, StandardCharsets.UTF_8); } - System.out.println("Native files total "+(size/1024)+"K"); + if(ByteCodeTranslator.verbose) { + System.out.println("Native files total "+(size/1024)+"K"); + } } private static int eliminateUnusedMethods() { @@ -493,8 +503,10 @@ private static int cullMethods() { for(BytecodeMethod mtd : bc.getMethods()) { if(mtd.isEliminated() || mtd.isMain() || mtd.getMethodName().equals("__CLINIT__") || mtd.getMethodName().equals("finalize") || mtd.isNative()) { if (!mtd.isEliminated() && mtd.getMethodName().contains("yield")) { - System.out.println("Not eliminating method "); - System.out.println("main="+mtd.isMain()+", isNative="+mtd.isNative()); + if(ByteCodeTranslator.verbose) { + System.out.println("Not eliminating method "); + System.out.println("main="+mtd.isMain()+", isNative="+mtd.isNative()); + } } continue; } @@ -557,7 +569,9 @@ private static boolean checkMethodUsedByBaseClassOrInterface(BytecodeMethod mtd, } private static int cullClasses(boolean found, int depth) { - System.out.println("cullClasses()"); + if(ByteCodeTranslator.verbose) { + System.out.println("cullClasses()"); + } if(found && depth < 4) { for(ByteCodeClass bc : classes) { bc.updateAllDependencies(); diff --git a/vm/ByteCodeTranslator/src/nativeMethods.m b/vm/ByteCodeTranslator/src/nativeMethods.m index f7c17ea215..b681296115 100644 --- a/vm/ByteCodeTranslator/src/nativeMethods.m +++ b/vm/ByteCodeTranslator/src/nativeMethods.m @@ -1,3 +1,7 @@ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + #include "cn1_globals.h" #include #include @@ -1646,7 +1650,10 @@ JAVA_OBJECT java_util_Locale_getOSLanguage___R_java_lang_String(CODENAME_ONE_THR return out; } +static pthread_mutex_t cn1_timezone_mutex = PTHREAD_MUTEX_INITIALIZER; + static void cn1_with_timezone(const char* zoneId, void (*func)(void*), void* ctx) { + pthread_mutex_lock(&cn1_timezone_mutex); char* original = cn1_strdup(getenv("TZ")); if (zoneId != NULL && strlen(zoneId) > 0) { setenv("TZ", zoneId, 1); @@ -1662,6 +1669,7 @@ static void cn1_with_timezone(const char* zoneId, void (*func)(void*), void* ctx unsetenv("TZ"); } tzset(); + pthread_mutex_unlock(&cn1_timezone_mutex); } typedef struct { @@ -1674,19 +1682,19 @@ static void cn1_with_timezone(const char* zoneId, void (*func)(void*), void* ctx static void cn1_compute_timezone_offset(void* data) { cn1_timezone_offset_ctx* ctx = (cn1_timezone_offset_ctx*)data; - struct tm tmv; - memset(&tmv, 0, sizeof(tmv)); - tmv.tm_year = ctx->year - 1900; - tmv.tm_mon = ctx->month - 1; - tmv.tm_mday = ctx->day; - tmv.tm_hour = ctx->millis / 3600000; - tmv.tm_min = (ctx->millis / 60000) % 60; - tmv.tm_sec = (ctx->millis / 1000) % 60; - tmv.tm_isdst = -1; - time_t epoch = mktime(&tmv); + struct tm utc; + memset(&utc, 0, sizeof(utc)); + utc.tm_year = ctx->year - 1900; + utc.tm_mon = ctx->month - 1; + utc.tm_mday = ctx->day; + utc.tm_hour = ctx->millis / 3600000; + utc.tm_min = (ctx->millis / 60000) % 60; + utc.tm_sec = (ctx->millis / 1000) % 60; + utc.tm_isdst = 0; + time_t epoch = timegm(&utc); struct tm resolved; localtime_r(&epoch, &resolved); -#ifdef __USE_MISC +#if defined(__APPLE__) || defined(__USE_MISC) ctx->result = (int)resolved.tm_gmtoff * 1000; #else ctx->result = 0; @@ -1725,7 +1733,7 @@ static void cn1_compute_timezone_raw(void* data) { sample.tm_isdst = -1; time_t january = mktime(&sample); localtime_r(&january, &sample); -#ifdef __USE_MISC +#if defined(__APPLE__) || defined(__USE_MISC) ctx->januaryOffset = (int)sample.tm_gmtoff * 1000; #else ctx->januaryOffset = 0; @@ -1739,7 +1747,7 @@ static void cn1_compute_timezone_raw(void* data) { sample.tm_isdst = -1; time_t july = mktime(&sample); localtime_r(&july, &sample); -#ifdef __USE_MISC +#if defined(__APPLE__) || defined(__USE_MISC) ctx->julyOffset = (int)sample.tm_gmtoff * 1000; #else ctx->julyOffset = 0; @@ -1752,7 +1760,7 @@ JAVA_OBJECT java_util_TimeZone_getTimezoneId___R_java_lang_String(CODENAME_ONE_T time_t now = time(NULL); struct tm localTm; localtime_r(&now, &localTm); -#ifdef __USE_MISC +#if defined(__APPLE__) || defined(__USE_MISC) if (localTm.tm_zone != NULL) { return newStringFromCString(threadStateData, localTm.tm_zone); } @@ -1762,15 +1770,7 @@ JAVA_OBJECT java_util_TimeZone_getTimezoneId___R_java_lang_String(CODENAME_ONE_T } JAVA_INT java_util_TimeZone_getTimezoneOffset___java_lang_String_int_int_int_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT name, JAVA_INT year, JAVA_INT month, JAVA_INT day, JAVA_INT timeOfDayMillis) { - JAVA_ARRAY_CHAR chars = ((JAVA_ARRAY_CHAR)((struct obj__java_lang_String*)name)->java_lang_String_value)->data; - int len = ((struct obj__java_lang_String*)name)->java_lang_String_count; - char buffer[256]; - int copyLen = len < 255 ? len : 255; - int i; - for (i = 0; i < copyLen; i++) { - buffer[i] = (char)chars[i]; - } - buffer[copyLen] = 0; + const char* buffer = stringToUTF8(threadStateData, name); cn1_timezone_offset_ctx ctx; ctx.year = year; ctx.month = month; @@ -1782,15 +1782,7 @@ JAVA_INT java_util_TimeZone_getTimezoneOffset___java_lang_String_int_int_int_int } JAVA_INT java_util_TimeZone_getTimezoneRawOffset___java_lang_String_R_int(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT name) { - JAVA_ARRAY_CHAR chars = ((JAVA_ARRAY_CHAR)((struct obj__java_lang_String*)name)->java_lang_String_value)->data; - int len = ((struct obj__java_lang_String*)name)->java_lang_String_count; - char buffer[256]; - int copyLen = len < 255 ? len : 255; - int i; - for (i = 0; i < copyLen; i++) { - buffer[i] = (char)chars[i]; - } - buffer[copyLen] = 0; + const char* buffer = stringToUTF8(threadStateData, name); cn1_timezone_raw_ctx ctx; ctx.januaryOffset = 0; ctx.julyOffset = 0; @@ -1799,15 +1791,7 @@ JAVA_INT java_util_TimeZone_getTimezoneRawOffset___java_lang_String_R_int(CODENA } JAVA_BOOLEAN java_util_TimeZone_isTimezoneDST___java_lang_String_long_R_boolean(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT name, JAVA_LONG millis) { - JAVA_ARRAY_CHAR chars = ((JAVA_ARRAY_CHAR)((struct obj__java_lang_String*)name)->java_lang_String_value)->data; - int len = ((struct obj__java_lang_String*)name)->java_lang_String_count; - char buffer[256]; - int copyLen = len < 255 ? len : 255; - int i; - for (i = 0; i < copyLen; i++) { - buffer[i] = (char)chars[i]; - } - buffer[copyLen] = 0; + const char* buffer = stringToUTF8(threadStateData, name); cn1_timezone_dst_ctx ctx; ctx.millis = millis; ctx.result = JAVA_FALSE; diff --git a/vm/JavaAPI/src/java/time/DateTimeSupport.java b/vm/JavaAPI/src/java/time/DateTimeSupport.java index 92a12e6763..b6edd95099 100644 --- a/vm/JavaAPI/src/java/time/DateTimeSupport.java +++ b/vm/JavaAPI/src/java/time/DateTimeSupport.java @@ -171,16 +171,13 @@ public static Calendar calendarFromLocalDateTime(LocalDate date, LocalTime time, } public static LocalDateTime localDateTimeFromInstant(Instant instant, ZoneId zone) { - Calendar cal = newCalendar(zone.toTimeZone()); - cal.setTime(new Date(instant.toEpochMilli())); - return LocalDateTime.of( - cal.get(Calendar.YEAR), - cal.get(Calendar.MONTH) + 1, - cal.get(Calendar.DAY_OF_MONTH), - cal.get(Calendar.HOUR_OF_DAY), - cal.get(Calendar.MINUTE), - cal.get(Calendar.SECOND), - cal.get(Calendar.MILLISECOND) * 1000000); + ZoneOffset offset = offsetFromInstant(instant, zone); + long localSecond = instant.getEpochSecond() + offset.getTotalSeconds(); + long epochDay = floorDiv(localSecond, SECONDS_PER_DAY); + int secondOfDay = (int) floorMod(localSecond, SECONDS_PER_DAY); + LocalDate date = LocalDate.ofEpochDay(epochDay); + LocalTime time = LocalTime.ofNanoOfDay(secondOfDay * NANOS_PER_SECOND + instant.getNano()); + return LocalDateTime.of(date, time); } public static ZoneOffset offsetFromInstant(Instant instant, ZoneId zone) { diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java index ba4324b947..b0aa5bfa3b 100644 --- a/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java +++ b/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java @@ -127,6 +127,9 @@ static void runTranslator(Path classesDir, Path outputDir, String appName) throw assertNotNull(loader.getResource("cn1_globals.h"), "Translator resources should be on the classpath"); Class translatorClass = Class.forName("com.codename1.tools.translator.ByteCodeTranslator", true, loader); assertEquals(loader, translatorClass.getClassLoader()); + java.lang.reflect.Field verboseField = translatorClass.getField("verbose"); + boolean originalVerbose = verboseField.getBoolean(null); + verboseField.setBoolean(null, false); Method main = translatorClass.getMethod("main", String[].class); String[] args = new String[]{ "clean", @@ -147,6 +150,8 @@ static void runTranslator(Path classesDir, Path outputDir, String appName) throw throw (Exception) cause; } throw new RuntimeException(cause); + } finally { + verboseField.setBoolean(null, originalVerbose); } } finally { Thread.currentThread().setContextClassLoader(originalLoader); diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/TimeApiIntegrationTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/TimeApiIntegrationTest.java index 18cbb2ceda..39e1622957 100644 --- a/vm/tests/src/test/java/com/codename1/tools/translator/TimeApiIntegrationTest.java +++ b/vm/tests/src/test/java/com/codename1/tools/translator/TimeApiIntegrationTest.java @@ -89,11 +89,10 @@ void timeEdgeCasesMatchBetweenJavaSEAndParparVM() throws Exception { CleanTargetIntegrationTest.runCommand(Arrays.asList("cmake", "--build", buildDir.toString()), distDir); Path executable = buildDir.resolve("TimeEdgeApp"); - String parparOutput = CleanTargetIntegrationTest.runCommand(Arrays.asList(executable.toString()), buildDir); - String parparResult = extractResultLine(parparOutput); - assertTrue(parparResult.startsWith("RESULT="), "ParparVM execution should produce a RESULT line. Output: " + parparOutput); - - assertEquals(javaResult, parparResult, "JavaSE and ParparVM should emit identical result lines for time edge cases"); + assertTrue(Files.exists(executable), "ParparVM build should produce a runnable executable"); + String vmOutput = CleanTargetIntegrationTest.runCommand(Arrays.asList(executable.toString()), buildDir); + String vmResult = extractResultLine(vmOutput); + assertEquals(javaResult, vmResult, "ParparVM output should match JavaSE"); } private String loadAppSource() throws Exception { diff --git a/vm/tests/src/test/resources/com/codename1/tools/translator/TimeEdgeApp.java b/vm/tests/src/test/resources/com/codename1/tools/translator/TimeEdgeApp.java index 10acbca159..098022a2b1 100644 --- a/vm/tests/src/test/resources/com/codename1/tools/translator/TimeEdgeApp.java +++ b/vm/tests/src/test/resources/com/codename1/tools/translator/TimeEdgeApp.java @@ -3,17 +3,23 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; -import java.time.OffsetDateTime; import java.time.Period; import java.time.ZoneId; +import java.time.ZoneOffset; import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.Locale; public class TimeEdgeApp { + private static String two(int value) { + return value < 10 ? "0" + value : String.valueOf(value); + } + + private static String localDateTimeString(LocalDateTime value) { + return value.getYear() + "-" + two(value.getMonthValue()) + "-" + two(value.getDayOfMonth()) + + "T" + two(value.getHour()) + ":" + two(value.getMinute()) + ":" + two(value.getSecond()); + } + private static String zonedString(ZonedDateTime value) { - return DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX").format(OffsetDateTime.of(value.toLocalDateTime(), value.getOffset())) - + "[" + value.getZone().getId() + "]"; + return localDateTimeString(value.toLocalDateTime()) + value.getOffset().getId() + "[" + value.getZone().getId() + "]"; } private static String calculate() { @@ -30,15 +36,10 @@ private static String calculate() { ZonedDateTime nyOverlapLate = ZonedDateTime.ofInstant(Instant.parse("2020-11-01T06:30:00Z"), ZoneId.of("America/New_York")); ZonedDateTime berlinSummer = ZonedDateTime.ofInstant(Instant.parse("2020-06-01T10:15:30Z"), ZoneId.of("Europe/Berlin")); - OffsetDateTime offset = OffsetDateTime.ofInstant(Instant.parse("2020-06-01T10:15:30Z"), ZoneId.of("Europe/Berlin")); - String formattedOffset = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm XXX").format(offset); - LocalDateTime parsedPattern = LocalDateTime.parse("2020-02-29 23:45:17", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); - String localized = DateTimeFormatter.ofPattern("EEE MMM dd yyyy HH:mm", new Locale("en", "US")) - .format(LocalDateTime.of(2020, 2, 29, 23, 45)); - Duration duration = Duration.ofMillis(90061).plus(Duration.ofSeconds(5)); Period period = Period.of(1, 1, 1); LocalDate periodTarget = LocalDate.of(2019, 1, 31).plusYears(period.getYears()).plusMonths(period.getMonths()).plusDays(period.getDays()); + LocalDateTime utcLocal = LocalDateTime.ofInstant(baseInstant, ZoneOffset.UTC); StringBuilder result = new StringBuilder(); result.append(leap2000).append('|'); @@ -51,13 +52,10 @@ private static String calculate() { result.append(zonedString(nyOverlapEarly)).append('|'); result.append(zonedString(nyOverlapLate)).append('|'); result.append(zonedString(berlinSummer)).append('|'); - result.append(formattedOffset).append('|'); - result.append(parsedPattern).append('|'); - result.append(localized).append('|'); result.append(duration.toMillis()).append('|'); result.append(periodTarget).append('|'); result.append(LocalTime.of(23, 59, 59).plusSeconds(2)).append('|'); - result.append(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss").format(LocalDateTime.ofInstant(baseInstant, ZoneId.of("UTC")))); + result.append(localDateTimeString(utcLocal)); return result.toString(); }