diff --git a/realty-common/src/main/java/io/github/md5sha256/realty/api/DurationFormatter.java b/realty-common/src/main/java/io/github/md5sha256/realty/api/DurationFormatter.java index 009364d..d7f5031 100644 --- a/realty-common/src/main/java/io/github/md5sha256/realty/api/DurationFormatter.java +++ b/realty-common/src/main/java/io/github/md5sha256/realty/api/DurationFormatter.java @@ -1,8 +1,10 @@ package io.github.md5sha256.realty.api; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.time.Duration; +import java.time.LocalDateTime; public final class DurationFormatter { @@ -57,4 +59,27 @@ private DurationFormatter() {} } return sb.toString(); } + + /** + * Formats the remaining time until the given end date. + * + *
Returns {@code "N/A"} when no end date exists and {@code "Expired"} + * when the end date has already passed.
+ */ + public static @NotNull String formatTimeLeft(@Nullable LocalDateTime endDate) { + return formatTimeLeft(endDate, LocalDateTime.now()); + } + + static @NotNull String formatTimeLeft(@Nullable LocalDateTime endDate, @NotNull LocalDateTime now) { + if (endDate == null) { + return "N/A"; + } + + Duration remaining = Duration.between(now, endDate); + if (remaining.isZero() || remaining.isNegative()) { + return "Expired"; + } + + return format(remaining); + } } diff --git a/realty-common/src/main/java/io/github/md5sha256/realty/database/RealtyLogicImpl.java b/realty-common/src/main/java/io/github/md5sha256/realty/database/RealtyLogicImpl.java index 24dc8bf..81f8517 100644 --- a/realty-common/src/main/java/io/github/md5sha256/realty/database/RealtyLogicImpl.java +++ b/realty-common/src/main/java/io/github/md5sha256/realty/database/RealtyLogicImpl.java @@ -876,6 +876,7 @@ public record RegionInfo( placeholders.put("duration", DurationFormatter.format(Duration.ofSeconds(lease.durationSeconds()))); placeholders.put("start_date", lease.startDate() != null ? dateFormatter.apply(lease.startDate()) : "N/A"); placeholders.put("end_date", lease.endDate() != null ? dateFormatter.apply(lease.endDate()) : "N/A"); + placeholders.put("time_left", DurationFormatter.formatTimeLeft(lease.endDate())); if (lease.maxExtensions() != null) { placeholders.put("extensions", lease.currentMaxExtensions() + "/" + lease.maxExtensions()); } else { diff --git a/realty-common/src/test/java/io/github/md5sha256/realty/api/DurationFormatterTest.java b/realty-common/src/test/java/io/github/md5sha256/realty/api/DurationFormatterTest.java new file mode 100644 index 0000000..40aca39 --- /dev/null +++ b/realty-common/src/test/java/io/github/md5sha256/realty/api/DurationFormatterTest.java @@ -0,0 +1,37 @@ +package io.github.md5sha256.realty.api; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +class DurationFormatterTest { + + @Test + @DisplayName("formatTimeLeft returns N/A when no end date exists") + void noEndDate() { + String result = DurationFormatter.formatTimeLeft(null, LocalDateTime.of(2026, 3, 25, 1, 0, 0)); + Assertions.assertEquals("N/A", result); + } + + @Test + @DisplayName("formatTimeLeft returns Expired when end date has passed") + void expired() { + String result = DurationFormatter.formatTimeLeft( + LocalDateTime.of(2026, 3, 25, 1, 0, 0), + LocalDateTime.of(2026, 3, 25, 1, 0, 1) + ); + Assertions.assertEquals("Expired", result); + } + + @Test + @DisplayName("formatTimeLeft formats remaining time using the standard duration formatter") + void remainingTime() { + String result = DurationFormatter.formatTimeLeft( + LocalDateTime.of(2026, 3, 27, 4, 30, 15), + LocalDateTime.of(2026, 3, 25, 1, 0, 0) + ); + Assertions.assertEquals("2d 3h 30m 15s", result); + } +} diff --git a/realty-paper/src/main/java/io/github/md5sha256/realty/command/InfoCommand.java b/realty-paper/src/main/java/io/github/md5sha256/realty/command/InfoCommand.java index bb0cc43..94a8a1d 100644 --- a/realty-paper/src/main/java/io/github/md5sha256/realty/command/InfoCommand.java +++ b/realty-paper/src/main/java/io/github/md5sha256/realty/command/InfoCommand.java @@ -182,6 +182,7 @@ private void appendLeaseholdInfo(@NotNull TextComponent.Builder builder, Placeholder.unparsed("end_date", leasehold.endDate() != null ? DateFormatter.format(settings.get(), leasehold.endDate()) : "N/A"), + Placeholder.unparsed("time_left", DurationFormatter.formatTimeLeft(leasehold.endDate())), Placeholder.unparsed("extensions", extensions))); } diff --git a/realty-paper/src/main/java/io/github/md5sha256/realty/command/ListCommand.java b/realty-paper/src/main/java/io/github/md5sha256/realty/command/ListCommand.java index 35444d3..58c49ff 100644 --- a/realty-paper/src/main/java/io/github/md5sha256/realty/command/ListCommand.java +++ b/realty-paper/src/main/java/io/github/md5sha256/realty/command/ListCommand.java @@ -1,6 +1,8 @@ package io.github.md5sha256.realty.command; +import io.github.md5sha256.realty.api.DurationFormatter; import io.github.md5sha256.realty.database.RealtyLogicImpl; +import io.github.md5sha256.realty.database.entity.LeaseholdContractEntity; import io.github.md5sha256.realty.database.entity.RealtyRegionEntity; import io.github.md5sha256.realty.localisation.MessageContainer; import io.github.md5sha256.realty.localisation.MessageKeys; @@ -137,7 +139,7 @@ private void listAll(@NotNull CommandSender sender, @NotNull UUID targetId, builder.append(parseMiniMessage(MessageKeys.LIST_HEADER, "