Skip to content

Commit 73d38ff

Browse files
committed
DateTimeUtil: zone-explicit rendering; Instant-path Inception sentinel
The one home for time rendering had two half-complete paths: the epoch path carried all four sentinel words but always rendered at the system default zone (no UTC option, so any non-sentinel time in a generated artifact was machine-dependent), while the Instant/formatter path rendered UTC but was missing the Inception sentinel the resolution overload already had. The sentinel handling now lives in one zone-explicit method -- format(long, formatter, zone) -- with the zone-implicit overloads delegating at system default for interactive surfaces and formatUtc(long) delegating at UTC for generated artifacts. KonceptExtractor since and retiredAt render through formatUtc, making the extraction timezone-independent; no visible change today, since every IKE stamp is the inception sentinel. No scattered special-casing anywhere: callers keep calling DateTimeUtil. Refs: IKE-Network/ike-issues#897
1 parent 9e5e5fe commit 73d38ff

3 files changed

Lines changed: 58 additions & 5 deletions

File tree

common/src/main/java/dev/ikm/tinkar/common/util/time/DateTimeUtil.java

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,36 @@ public static String format(long epochMilliSecond) {
122122
return format(epochMilliSecond, FORMATTER);
123123
}
124124
public static String format(long epochMilliSecond, DateTimeFormatter formatter) {
125+
return format(epochMilliSecond, formatter, ZoneId.systemDefault());
126+
}
127+
128+
/**
129+
* Formats an epoch time in UTC with the default {@link #FORMATTER} — the form for
130+
* generated artifacts, whose text must not depend on the generating machine's zone
131+
* (IKE-Network/ike-issues#897). Sentinel times render as their words
132+
* ({@code Latest}, {@code Canceled}, {@code Premundane}, {@code Inception}),
133+
* identically to every other format path.
134+
*
135+
* @param epochMilliSecond the epoch time in milliseconds, or a sentinel value
136+
* @return the sentinel word, or the UTC-rendered time
137+
*/
138+
public static String formatUtc(long epochMilliSecond) {
139+
return format(epochMilliSecond, FORMATTER, ZoneOffset.UTC);
140+
}
141+
142+
/**
143+
* Formats an epoch time at an explicit zone — the one method carrying the sentinel
144+
* handling for the epoch path; the zone-implicit overloads delegate here at
145+
* {@link ZoneId#systemDefault()} (interactive surfaces render local), and
146+
* {@link #formatUtc(long)} delegates at UTC (generated artifacts render
147+
* machine-independently).
148+
*
149+
* @param epochMilliSecond the epoch time in milliseconds, or a sentinel value
150+
* @param formatter the date-time formatter to render with
151+
* @param zone the zone to render the instant at
152+
* @return the sentinel word, or the time rendered at {@code zone}
153+
*/
154+
public static String format(long epochMilliSecond, DateTimeFormatter formatter, ZoneId zone) {
125155
if (epochMilliSecond == Long.MAX_VALUE) {
126156
return LATEST;
127157
}
@@ -134,9 +164,7 @@ public static String format(long epochMilliSecond, DateTimeFormatter formatter)
134164
if (epochMilliSecond == PrimitiveData.INCEPTION_EPOCH) {
135165
return INCEPTION;
136166
}
137-
ZonedDateTime positionTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(epochMilliSecond), ZoneOffset.UTC);
138-
ZonedDateTime inLocalZone = positionTime.withZoneSameInstant(ZoneId.systemDefault());
139-
return inLocalZone.format(formatter);
167+
return ZonedDateTime.ofInstant(Instant.ofEpochMilli(epochMilliSecond), zone).format(formatter);
140168
}
141169
public static String timeNowSimple() {
142170
return TIME_SIMPLE.format(ZonedDateTime.now());
@@ -251,6 +279,9 @@ public static String format(Instant instant, DateTimeFormatter formatter) {
251279
if (instant.equals(PrimitiveData.INCEPTION_INSTANT)) {
252280
return INCEPTION;
253281
}
282+
if (instant.equals(PrimitiveData.INCEPTION_INSTANT)) {
283+
return INCEPTION;
284+
}
254285
return formatter.format(instant.atOffset(ZoneOffset.UTC));
255286
}
256287
public static String format(Instant instant) {

common/src/test/java/dev/ikm/tinkar/common/util/time/DateTimeUtilTest.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,28 @@ void format() {
5151
assertEquals(INCEPTION, DateTimeUtil.format(PrimitiveData.INCEPTION_EPOCH));
5252
}
5353

54+
@Test
55+
void zoneExplicitRendering() {
56+
// A real time renders differently across zones through the zone-explicit form...
57+
long epoch = Instant.parse("2027-03-05T04:30:00Z").toEpochMilli();
58+
assertEquals("2027-03-05 04:30",
59+
DateTimeUtil.format(epoch, DateTimeUtil.FORMATTER, java.time.ZoneOffset.UTC));
60+
assertEquals("2027-03-04 20:30",
61+
DateTimeUtil.format(epoch, DateTimeUtil.FORMATTER, java.time.ZoneId.of("America/Los_Angeles")));
62+
assertEquals("2027-03-05 04:30", DateTimeUtil.formatUtc(epoch),
63+
"formatUtc is the machine-independent form for generated artifacts"
64+
+ " (IKE-Network/ike-issues#897)");
65+
66+
// ...while the sentinel words are zone-invariant on every path.
67+
assertEquals(INCEPTION, DateTimeUtil.formatUtc(PrimitiveData.INCEPTION_EPOCH));
68+
assertEquals(PREMUNDANE, DateTimeUtil.formatUtc(PrimitiveData.PREMUNDANE_TIME));
69+
assertEquals(LATEST, DateTimeUtil.formatUtc(Long.MAX_VALUE));
70+
assertEquals(CANCELED, DateTimeUtil.formatUtc(Long.MIN_VALUE));
71+
assertEquals(INCEPTION,
72+
DateTimeUtil.format(PrimitiveData.INCEPTION_INSTANT, DateTimeUtil.SEC_FORMATTER),
73+
"the Instant/formatter path carries the inception sentinel like every other path");
74+
}
75+
5476
@Test
5577
void inceptionEpochIsAMillisecondAfterMidnight() {
5678
// The .777 offset is deliberate: a real calendar date landing exactly at

entity/src/main/java/dev/ikm/tinkar/entity/builder/KonceptExtractor.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ public static String extractYaml(Integer narrativePatternNid) {
239239

240240
long since = earliestStampTime(nid);
241241
if (since != Long.MAX_VALUE) {
242-
sb.append(" since: ").append(yaml(DateTimeUtil.format(since))).append('\n');
242+
sb.append(" since: ").append(yaml(DateTimeUtil.formatUtc(since))).append('\n');
243243
}
244244
List<String> comments = activeComments(nid, calculator);
245245
if (!comments.isEmpty()) {
@@ -254,7 +254,7 @@ public static String extractYaml(Integer narrativePatternNid) {
254254
for (RetiredComment retiredComment : retired) {
255255
sb.append(" - text: ").append(yaml(retiredComment.text())).append('\n');
256256
sb.append(" retiredAt: ")
257-
.append(yaml(DateTimeUtil.format(retiredComment.retiredAt()))).append('\n');
257+
.append(yaml(DateTimeUtil.formatUtc(retiredComment.retiredAt()))).append('\n');
258258
}
259259
}
260260
if (narrativePatternNid != null) {

0 commit comments

Comments
 (0)