diff --git a/src/java.base/share/classes/java/time/Year.java b/src/java.base/share/classes/java/time/Year.java index df4707e8b32..cdcaa390320 100644 --- a/src/java.base/share/classes/java/time/Year.java +++ b/src/java.base/share/classes/java/time/Year.java @@ -315,7 +315,10 @@ public final class Year * @return true if the year is leap, false otherwise */ public static boolean isLeap(long year) { - return ((year & 3) == 0) && ((year % 100) != 0 || (year % 400) == 0); + // A year that is a multiple of 100, 200 and 300 is not divisible by 16, but 400 is. + // So for a year that's divisible by 4, checking that it's also divisible by 16 + // is sufficient to determine it must be a leap year. + return (year & 15) == 0 ? (year & 3) == 0 : (year & 3) == 0 && year % 100 != 0; } //----------------------------------------------------------------------- diff --git a/src/java.base/share/classes/java/time/chrono/IsoChronology.java b/src/java.base/share/classes/java/time/chrono/IsoChronology.java index caeb980056b..d2590ad3158 100644 --- a/src/java.base/share/classes/java/time/chrono/IsoChronology.java +++ b/src/java.base/share/classes/java/time/chrono/IsoChronology.java @@ -475,7 +475,7 @@ public final class IsoChronology extends AbstractChronology implements Serializa */ @Override public boolean isLeapYear(long prolepticYear) { - return ((prolepticYear & 3) == 0) && ((prolepticYear % 100) != 0 || (prolepticYear % 400) == 0); + return Year.isLeap(prolepticYear); } @Override diff --git a/src/java.base/share/classes/java/util/GregorianCalendar.java b/src/java.base/share/classes/java/util/GregorianCalendar.java index d6e559fabfc..c9f25edbab7 100644 --- a/src/java.base/share/classes/java/util/GregorianCalendar.java +++ b/src/java.base/share/classes/java/util/GregorianCalendar.java @@ -826,7 +826,10 @@ public class GregorianCalendar extends Calendar { } if (year > gregorianCutoverYear) { - return (year%100 != 0) || (year%400 == 0); // Gregorian + // A multiple of 100, 200 and 300 is not divisible by 16, but 400 is. + // So for a year that's divisible by 4, checking that it's also divisible by 16 + // is sufficient to determine it must be a leap year. + return (year & 15) == 0 || (year % 100 != 0); // Gregorian } if (year < gregorianCutoverYearJulian) { return true; // Julian @@ -840,7 +843,7 @@ public class GregorianCalendar extends Calendar { } else { gregorian = year == gregorianCutoverYear; } - return gregorian ? (year%100 != 0) || (year%400 == 0) : true; + return !gregorian || (year & 15) == 0 || (year % 100 != 0); } /** diff --git a/src/java.base/share/classes/sun/util/calendar/CalendarUtils.java b/src/java.base/share/classes/sun/util/calendar/CalendarUtils.java index 60c2decc48b..4cbb76455a9 100644 --- a/src/java.base/share/classes/sun/util/calendar/CalendarUtils.java +++ b/src/java.base/share/classes/sun/util/calendar/CalendarUtils.java @@ -40,8 +40,12 @@ public final class CalendarUtils { * @see CalendarDate#isLeapYear */ public static boolean isGregorianLeapYear(int gregorianYear) { - return (((gregorianYear % 4) == 0) && (((gregorianYear % 100) != 0) - || ((gregorianYear % 400) == 0))); + // A year that is a multiple of 100, 200 and 300 is not divisible by 16, but 400 is. + // So for a year that's divisible by 4, checking that it's also divisible by 16 + // is sufficient to determine it must be a leap year. + return (gregorianYear & 15) == 0 + ? (gregorianYear & 3) == 0 + : (gregorianYear & 3) == 0 && gregorianYear % 100 != 0; } /** diff --git a/src/java.base/share/classes/sun/util/calendar/ZoneInfoFile.java b/src/java.base/share/classes/sun/util/calendar/ZoneInfoFile.java index 17c02706336..6abc5b6eec5 100644 --- a/src/java.base/share/classes/sun/util/calendar/ZoneInfoFile.java +++ b/src/java.base/share/classes/sun/util/calendar/ZoneInfoFile.java @@ -929,7 +929,7 @@ public final class ZoneInfoFile { } static final boolean isLeapYear(int year) { - return ((year & 3) == 0) && ((year % 100) != 0 || (year % 400) == 0); + return CalendarUtils.isGregorianLeapYear(year); } static final int lengthOfMonth(int year, int month) { diff --git a/test/micro/org/openjdk/bench/java/time/LeapYearBench.java b/test/micro/org/openjdk/bench/java/time/LeapYearBench.java new file mode 100644 index 00000000000..d7a00ba3bc2 --- /dev/null +++ b/test/micro/org/openjdk/bench/java/time/LeapYearBench.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package org.openjdk.bench.java.time; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +import java.time.Duration; +import java.time.Instant; +import java.time.Year; +import java.time.ZonedDateTime; +import java.time.chrono.IsoChronology; +import java.time.temporal.ChronoUnit; +import java.util.GregorianCalendar; +import java.util.Random; +import java.util.concurrent.TimeUnit; +import java.util.stream.IntStream; + +/** + * Examine Year.leapYear-related operations + */ +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 1) +@Fork(3) +@State(Scope.Thread) +public class LeapYearBench { + + private long[] years; + + private GregorianCalendar calendar; + + @Setup + public void createInstants() { + // Large enough number of years to guarantee that the distribution of + // leap years is reasonably realistic + years = new long[2048]; + final Random random = new Random(0); + for (int i = 0; i < years.length; i++) { + years[i] = random.nextLong(2000) + 2000; + } + calendar = GregorianCalendar.from(ZonedDateTime.now()); + } + + @Benchmark + public void isLeapYear(Blackhole bh) { + for (long year : years) { + bh.consume(Year.isLeap(year)); + } + } + + @Benchmark + public void isLeapYearChrono(Blackhole bh) { + for (long year : years) { + bh.consume(IsoChronology.INSTANCE.isLeapYear(year)); + } + } + + @Benchmark + public void isLeapYearGregorian(Blackhole bh) { + for (long year : years) { + bh.consume(calendar.isLeapYear((int)year)); + } + } + + public static boolean isLeapNeriSchneider(long year) { + int d = year % 100 != 0 ? 4 : 16; + return (year & (d - 1)) == 0; + } + + @Benchmark + public void isLeapYearNS(Blackhole bh) { + for (long year : years) { + bh.consume(isLeapNeriSchneider(year)); + } + } +}