Java 21实现英国电力市场结算时段计算(适配夏令时转换)
英国电力市场结算时段的Java 21实现
需求说明
需实现一个方法,计算从英国当地当日午夜到指定时刻已完成的半小时间隔数(即结算时段编号),规则如下:
- 正常日:48个时段,编号1~48
- 短日(夏令时开始,时钟调快1小时):46个时段,编号1~46(跳过1小时对应的两个半小时间隔)
- 长日(夏令时结束,时钟调慢1小时):50个时段,编号1~50(重复1小时对应的两个半小时间隔)
方法签名:
int settlementPeriod(final OffsetDateTime date)
原有实现的问题
首次尝试直接计算伦敦时区当日起始到目标时间的实际时长,仅在正常日有效,短日/长日因夏令时转换导致时段编号错误:
public static int settlementPeriod(final OffsetDateTime readingTime) { final var zone = ZoneId.of("Europe/London"); final var startOfDay = readingTime.truncatedTo(ChronoUnit.DAYS).atZoneSameInstant(zone); final var duration = Duration.between(startOfDay, readingTime.atZoneSameInstant(zone)); return (int) duration.dividedBy(Duration.ofMinutes(30)) + 1; }
第二次尝试通过判断时区转换优化,但存在代码冗余、转换时间点判断错误的问题。
正确实现方案
基于java.time API处理夏令时的间隙(gap)和重叠(overlap)场景,调整本地时间的总分钟数后计算时段编号:
import java.time.*; import java.time.temporal.ChronoUnit; import java.util.Objects; public class SettlementPeriodCalculator { private static final ZoneId LONDON_ZONE = ZoneId.of("Europe/London"); public static int settlementPeriod(final OffsetDateTime readingTime) { Objects.requireNonNull(readingTime, "readingTime must not be null"); // 转换为伦敦时区的时间 ZonedDateTime zdt = readingTime.atZoneSameInstant(LONDON_ZONE); LocalDate localDate = zdt.toLocalDate(); LocalTime localTime = zdt.toLocalTime(); ZoneOffset offset = zdt.getOffset(); ZoneRules zoneRules = LONDON_ZONE.getRules(); // 计算本地时间从午夜开始的总分钟数 int totalMinutes = (int) localTime.toSecondOfDay() / 60; // 处理短日:夏令时开始,本地时间跳过01:00-02:00(间隙) ZoneOffsetTransition forwardTransition = zoneRules.getTransition(localDate.atStartOfDay(LONDON_ZONE).toInstant()); if (forwardTransition != null && forwardTransition.isGap()) { LocalTime gapStart = forwardTransition.getDateTimeBefore().toLocalTime(); LocalTime gapEnd = forwardTransition.getDateTimeAfter().toLocalTime(); // 若当前时间在间隙之后,减去间隙时长(60分钟) if (localTime.compareTo(gapEnd) >= 0) { totalMinutes -= (int) ChronoUnit.MINUTES.between(gapStart, gapEnd); } } // 处理长日:夏令时结束,本地时间重复01:00-02:00(重叠) ZoneOffsetTransition backwardTransition = zoneRules.getTransition(localDate.plusDays(1).atStartOfDay(LONDON_ZONE).toInstant()); if (backwardTransition != null && backwardTransition.isOverlap()) { LocalTime overlapStart = backwardTransition.getDateTimeBefore().toLocalTime(); LocalTime overlapEnd = backwardTransition.getDateTimeAfter().toLocalTime(); // 若当前时间在重叠区间内,且是第二个实例(偏移更小的GMT),加上重叠时长(60分钟) if (localTime.compareTo(overlapStart) >= 0 && localTime.compareTo(overlapEnd) < 0) { if (offset.equals(backwardTransition.getOffsetAfter())) { totalMinutes += (int) ChronoUnit.MINUTES.between(overlapStart, overlapEnd); } } } // 计算已完成的半小时间隔数,加1得到时段编号 return (totalMinutes / 30) + 1; } }
逻辑说明
- 时区转换:将输入的
OffsetDateTime转换为伦敦时区的ZonedDateTime,获取本地日期、时间及时区偏移。 - 基础分钟数计算:统计本地时间从午夜开始的总分钟数。
- 短日处理:判断当日是否存在夏令时开始的间隙(本地时间跳过1小时),若当前时间在间隙之后,减去间隙时长修正总分钟数。
- 长日处理:判断次日起始时刻是否存在夏令时结束的重叠(本地时间重复1小时),若当前时间是重叠区间的第二个实例(GMT偏移),加上重叠时长修正总分钟数。
- 时段计算:用修正后的总分钟数除以30,加1得到最终的结算时段编号。
测试场景验证
- 正常日:
2024-04-01T00:30+01:00→ 总分钟数30 → 时段编号2,符合预期。 - 短日:
2024-03-31T03:00+01:00→ 修正后总分钟数120 → 时段编号5,符合短日跳过两个时段的规则。 - 长日(重复时间第二次):
2024-10-27T01:30Z(GMT)→ 修正后总分钟数150 → 时段编号6,符合长日新增两个时段的规则。
内容的提问来源于stack exchange,提问作者Boris the Spider
相关产品推荐
相关产品推荐

