使用ZoneId获取的下拉列表太长,如何简化列表?

The pulldown list get using ZoneId is too long, how to simplify the list?

使用SpringZoneId,代码为:

 private static Map<String, String> getAllZoneIds() {
    final List<String> zoneList = new ArrayList<>(ZoneId.getAvailableZoneIds());
    final Map<String, String> zones = new HashMap<>();

    final LocalDateTime dt = LocalDateTime.now();

    for (final String zoneId : zoneList) {

        final ZoneId zone = ZoneId.of(zoneId);
        final ZonedDateTime zdt = dt.atZone(zone);
        final ZoneOffset zos = zdt.getOffset();

        //replace Z to +00:00
        final String offset = zos.getId().replaceAll("Z", "+00:00");

        zones.put(zone.toString(), offset);

    }

    final Map<String, String> sortZones = new LinkedHashMap<>();
    zones.entrySet().stream().sorted((left, right) -> {
        final String leftValue = left.getValue();
        final String leftKey = left.getKey();
        final String rightValue = right.getValue();
        final String rightKey = right.getKey();

        if (leftValue.equalsIgnoreCase(rightValue)) {
            return leftKey.compareTo(rightKey);
        } else {
            if (leftValue.charAt(0) == '+' &&  leftValue.charAt(0) == rightValue.charAt(0)) {
                return leftValue.compareTo(rightValue);
            } else {
                return rightValue.compareTo(leftValue);
            }
        }
    }).forEachOrdered(e -> {
        sortZones.put(e.getKey(), e.getKey() + " (UTC" + e.getValue() + ")");
    });
    System.out.println(sortZones);
    return sortZones;
}

输出为:

....
Antarctica/Casey->Antarctica/Casey (UTC+08:00)
Asia/Brunei->Asia/Brunei (UTC+08:00)
Asia/Chongqing->Asia/Chongqing (UTC+08:00)
Asia/Chungking->Asia/Chungking (UTC+08:00)
Asia/Harbin->Asia/Harbin (UTC+08:00)
Asia/Hong_Kong->Asia/Hong_Kong (UTC+08:00)
Asia/Hovd->Asia/Hovd (UTC+08:00)
Asia/Irkutsk->Asia/Irkutsk (UTC+08:00)
Asia/Kuala_Lumpur->Asia/Kuala_Lumpur (UTC+08:00)
Asia/Kuching->Asia/Kuching (UTC+08:00)
Asia/Macao->Asia/Macao (UTC+08:00)
Asia/Macau->Asia/Macau (UTC+08:00)
Asia/Makassar->Asia/Makassar (UTC+08:00)
Asia/Manila->Asia/Manila (UTC+08:00)
Asia/Shanghai->Asia/Shanghai (UTC+08:00)
Asia/Singapore->Asia/Singapore (UTC+08:00)
Asia/Taipei->Asia/Taipei (UTC+08:00)
Asia/Ujung_Pandang->Asia/Ujung_Pandang (UTC+08:00)
Australia/Perth->Australia/Perth (UTC+08:00)
Australia/West->Australia/West (UTC+08:00)
Etc/GMT-8->Etc/GMT-8 (UTC+08:00)
Hongkong->Hongkong (UTC+08:00)
PRC->PRC (UTC+08:00)
Singapore->Singapore (UTC+08:00)
Asia/Pyongyang->Asia/Pyongyang (UTC+08:30)
....

但是这个列表太长了,包含了500多个选项,而且充满了重复。比如重庆存在两次:

Asia/Chongqing->Asia/Chongqing (UTC+08:00)
Asia/Chungking->Asia/Chungking (UTC+08:00)

如何使用这个来缩短列表?

由于 backward file,某些区域出现两次:此文件映射已更改的时区名称,但旧名称保留为同义词,这可能是由于追溯兼容性原因。如果您查看该文件,您会发现这些条目:

Link    Asia/Shanghai       Asia/Chongqing
Link    Asia/Shanghai       Asia/Chungking

这意味着 Asia/ShanghaiAsia/ChungkingAsia/Chongqing 时区的当前时区名称。检查两个区域是否相同的一种方法是比较它们各自的 ZoneRules:

ZoneId chungking = ZoneId.of("Asia/Chungking");
ZoneId chongqing = ZoneId.of("Asia/Chongqing");
ZoneId shanghai = ZoneId.of("Asia/Shanghai");
System.out.println(chungking.getRules().equals(chongqing.getRules()));
System.out.println(chungking.getRules().equals(shanghai.getRules()));

两次比较都打印true,这意味着上面的所有ZoneId对象都代表相同的时区。不幸的是,直接比较 ZoneId 对象是行不通的,只有通过比较 ZoneRules 我们才能知道两个区域是否相同。

因此,减少列表的一种方法是忽略同义词。如果两个或多个区域具有相同的ZoneRules,您可以只考虑其中一个在列表中。

为此,您可以创建一个辅助 class 来包装 ZoneId,并具有一个 equals 方法来比较 ZoneRules(以及 hashcode 方法,因为它是 good practice to properly implement both).

public class UniqueZone {

    private ZoneId zone;

    public UniqueZone(ZoneId zone) {
        this.zone = zone;
    }

    public ZoneId getZone() {
        return zone;
    }

    // hashcode and equals use the ZoneRules
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((zone == null) ? 0 : zone.getRules().hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (!(obj instanceof UniqueZone))
            return false;
        UniqueZone other = (UniqueZone) obj;
        if (zone == null) {
            if (other.zone != null)
                return false;
        // two zones are equal if the ZoneRules are the same
        } else if (!zone.getRules().equals(other.zone.getRules()))
            return false;
        return true;
    }
}

然后我使用所有可用的区域 ID 创建 SetUniqueZone 个实例:

Set<UniqueZone> uniqueZones = ZoneId
    // get all IDs
    .getAvailableZoneIds().stream()
    // map to a UniqueZone (so zones with the same ZoneRules won't be duplicated)
    .map(zoneName -> new UniqueZone(ZoneId.of(zoneName)))
    // create Set
    .collect(Collectors.toSet());

这将使用 equals 方法中定义的条件(如果两个 ZoneId 实例具有相同的 ZoneRules,它们被认为是相同的,并且只插入一个在Set)。

现在我们只需要将这个 Set 映射回 List:

List<String> zoneList = uniqueZones.stream()
    // map back to the zoneId
    .map(u -> u.getZone().getId())
    // create list
    .collect(Collectors.toList());

现在您可以像现在一样使用 zoneList


通过上面的代码,在JDK1.8.0_144中,列表有385个元素,只有Asia/Chungking在列表中。当我使用流时,您无法真正预测列表中的三个(Asia/ChungkingAsia/ChongqingAsia/Shanghai)中的哪一个。

如果您想对其进行一些控制,您可以通过创建要排除的名称列表来明确过滤不需要的名称。假设我想排除 Asia/ChungkingAsia/Chongqing(因此只有 Asia/Shanghai 会出现在列表中)。我能做到:

// zone names to be excluded
Set<String> excludedNames = new HashSet<>();
excludedNames.add("Asia/Chungking");
excludedNames.add("Asia/Chongqing");

Set<UniqueZone> uniqueZones = ZoneId
    // get all IDs
    .getAvailableZoneIds().stream()
    // filter names I don't want
    .filter(zoneName -> ! excludedNames.contains(zoneName))
    // map to a UniqueZone (so zones with the same ZoneRules won't be duplicated)
    .map(zoneName -> new UniqueZone(ZoneId.of(zoneName)))
    // create Set
    .collect(Collectors.toSet());

有了这个,列表中只有 Asia/Shanghai

PS: 如果这个排除名称列表足够长以涵盖所有同义词情况,您甚至不需要 UniqueZone class(过滤器将完成所有工作)。但是您需要事先知道要排除的所有同义词。