Merge "Combine two versions of lookupByOffsetWithBias()"
diff --git a/luni/src/main/java/libcore/timezone/CountryTimeZones.java b/luni/src/main/java/libcore/timezone/CountryTimeZones.java
index e0676fe..0ef631c 100644
--- a/luni/src/main/java/libcore/timezone/CountryTimeZones.java
+++ b/luni/src/main/java/libcore/timezone/CountryTimeZones.java
@@ -347,85 +347,23 @@
     }
 
     /**
-     * Returns a time zone for the country, if there is one, that has the desired properties. If
-     * there are multiple matches and the {@code bias} is one of them then it is returned, otherwise
-     * an arbitrary match is returned based on the {@link #getTimeZoneMappings()} ordering.
-     *
-     * @param offsetMillis the offset from UTC at {@code whenMillis}
-     * @param isDst whether the zone is in DST
-     * @param whenMillis the UTC time to match against
-     * @param bias the time zone to prefer, can be null
-     * @deprecated Use {@link #lookupByOffsetWithBias(int, Integer, long, TimeZone)} instead
-     */
-    @libcore.api.CorePlatformApi
-    @Deprecated
-    public OffsetResult lookupByOffsetWithBias(int offsetMillis, boolean isDst, long whenMillis,
-            TimeZone bias) {
-        List<TimeZoneMapping> timeZoneMappings = getEffectiveTimeZoneMappingsAt(whenMillis);
-        if (timeZoneMappings.isEmpty()) {
-            return null;
-        }
-
-        TimeZone firstMatch = null;
-        boolean biasMatched = false;
-        boolean oneMatch = true;
-        for (TimeZoneMapping timeZoneMapping : timeZoneMappings) {
-            TimeZone match = timeZoneMapping.getTimeZone();
-            if (match == null || !offsetMatchesAtTime(match, offsetMillis, isDst, whenMillis)) {
-                continue;
-            }
-
-            if (firstMatch == null) {
-                firstMatch = match;
-            } else {
-                oneMatch = false;
-            }
-            if (bias != null && match.getID().equals(bias.getID())) {
-                biasMatched = true;
-            }
-            if (firstMatch != null && !oneMatch && (bias == null || biasMatched)) {
-                break;
-            }
-        }
-        if (firstMatch == null) {
-            return null;
-        }
-
-        TimeZone toReturn = biasMatched ? bias : firstMatch;
-        return new OffsetResult(toReturn, oneMatch);
-    }
-
-    /**
-     * Returns {@code true} if the specified offset, DST state and time would be valid in the
-     * timeZone.
-     */
-    private static boolean offsetMatchesAtTime(TimeZone timeZone, int offsetMillis, boolean isDst,
-            long whenMillis) {
-        int[] offsets = new int[2];
-        timeZone.getOffset(whenMillis, false /* local */, offsets);
-
-        // offsets[1] == 0 when the zone is not in DST.
-        boolean zoneIsDst = offsets[1] != 0;
-        if (isDst != zoneIsDst) {
-            return false;
-        }
-        return offsetMillis == (offsets[0] + offsets[1]);
-    }
-
-    /**
-     * Returns a time zone for the country, if there is one, that has the desired properties. If
+     * Returns a time zone for the country, if there is one, that matches the desired properties. If
      * there are multiple matches and the {@code bias} is one of them then it is returned, otherwise
      * an arbitrary match is returned based on the {@link #getEffectiveTimeZoneMappingsAt(long)}
      * ordering.
      *
-     * @param offsetMillis the offset from UTC at {@code whenMillis}
-     * @param dstOffsetMillis the part of {@code offsetMillis} contributed by DST, {@code null}
-     *                        means unknown
+     * @param totalOffsetMillis the offset from UTC at {@code whenMillis}
+     * @param isDst the Daylight Savings Time state at {@code whenMillis}. {@code true} means DST,
+     *     {@code false} means not DST, {@code null} means unknown
+     * @param dstOffsetMillis the part of {@code totalOffsetMillis} contributed by DST, only used if
+     *     {@code isDst} is {@code true}. The value can be {@code null} if the DST offset is
+     *     unknown
      * @param whenMillis the UTC time to match against
-     * @param bias the time zone to prefer, can be null
+     * @param bias the time zone to prefer, can be {@code null}
      */
-    public OffsetResult lookupByOffsetWithBias(int offsetMillis, Integer dstOffsetMillis,
-            long whenMillis, TimeZone bias) {
+    @libcore.api.CorePlatformApi
+    public OffsetResult lookupByOffsetWithBias(int totalOffsetMillis, Boolean isDst,
+            Integer dstOffsetMillis, long whenMillis, TimeZone bias) {
         List<TimeZoneMapping> timeZoneMappings = getEffectiveTimeZoneMappingsAt(whenMillis);
         if (timeZoneMappings.isEmpty()) {
             return null;
@@ -436,8 +374,8 @@
         boolean oneMatch = true;
         for (TimeZoneMapping timeZoneMapping : timeZoneMappings) {
             TimeZone match = timeZoneMapping.getTimeZone();
-            if (match == null ||
-                    !offsetMatchesAtTime(match, offsetMillis, dstOffsetMillis, whenMillis)) {
+            if (match == null || !offsetMatchesAtTime(match, totalOffsetMillis, isDst,
+                    dstOffsetMillis, whenMillis)) {
                 continue;
             }
 
@@ -462,20 +400,34 @@
     }
 
     /**
-     * Returns {@code true} if the specified offset, DST and time would be valid in the
-     * timeZone.
+     * Returns {@code true} if the specified {@code totalOffset}, {@code isDst},
+     * {@code dstOffsetMillis} would be valid in the {@code timeZone} at time {@code whenMillis}.
+     * {@code totalOffetMillis} is always matched.
+     * If {@code isDst} is {@code null} this means the DST state is unknown, so
+     * {@code dstOffsetMillis} is ignored.
+     * If {@code isDst} is {@code false}, {@code dstOffsetMillis} is ignored.
+     * If {@code isDst} is {@code true}, the DST state is considered. When considering DST state
+     * {@code dstOffsetMillis} can be {@code null} if it is unknown but when {@code dstOffsetMillis}
+     * is known then it is also matched.
      */
-    private static boolean offsetMatchesAtTime(TimeZone timeZone, int offsetMillis,
-            Integer dstOffsetMillis, long whenMillis) {
+    private static boolean offsetMatchesAtTime(TimeZone timeZone, int totalOffsetMillis,
+            Boolean isDst, Integer dstOffsetMillis, long whenMillis) {
         int[] offsets = new int[2];
         timeZone.getOffset(whenMillis, false /* local */, offsets);
 
-        if (dstOffsetMillis != null) {
-            if (dstOffsetMillis.intValue() != offsets[1]) {
-                return false;
-            }
+        if (totalOffsetMillis != (offsets[0] + offsets[1])) {
+            return false;
         }
-        return offsetMillis == (offsets[0] + offsets[1]);
+
+        if (isDst == null) {
+            return true;
+        } else if (!isDst) {
+            return offsets[1] == 0;
+        } else {
+            // isDst
+            return (dstOffsetMillis == null && offsets[1] != 0)
+                    || (dstOffsetMillis != null && dstOffsetMillis == offsets[1]);
+        }
     }
 
     private static String normalizeCountryIso(String countryIso) {
diff --git a/luni/src/main/java/libcore/timezone/TimeZoneFinder.java b/luni/src/main/java/libcore/timezone/TimeZoneFinder.java
index 04fddd4..252405b6 100644
--- a/luni/src/main/java/libcore/timezone/TimeZoneFinder.java
+++ b/luni/src/main/java/libcore/timezone/TimeZoneFinder.java
@@ -209,8 +209,8 @@
         if (countryTimeZones == null) {
             return null;
         }
-        CountryTimeZones.OffsetResult offsetResult =
-                countryTimeZones.lookupByOffsetWithBias(offsetMillis, isDst, whenMillis, bias);
+        CountryTimeZones.OffsetResult offsetResult = countryTimeZones.lookupByOffsetWithBias(
+                offsetMillis, isDst, null /* dstOffsetMillis */, whenMillis, bias);
         return offsetResult != null ? offsetResult.mTimeZone : null;
     }
 
diff --git a/luni/src/test/java/libcore/libcore/timezone/CountryTimeZonesTest.java b/luni/src/test/java/libcore/libcore/timezone/CountryTimeZonesTest.java
index 5131247..04f4811 100644
--- a/luni/src/test/java/libcore/libcore/timezone/CountryTimeZonesTest.java
+++ b/luni/src/test/java/libcore/libcore/timezone/CountryTimeZonesTest.java
@@ -20,8 +20,11 @@
 
 import android.icu.util.TimeZone;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Objects;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 import libcore.timezone.CountryTimeZones;
 import libcore.timezone.CountryTimeZones.OffsetResult;
@@ -39,13 +42,13 @@
 
     private static final String INVALID_TZ_ID = "Moon/Tranquility_Base";
 
-    // Zones used in the tests. NEW_YORK_TZ and LONDON_TZ chosen because they never overlap but both
-    // have DST.
-    private static final TimeZone NEW_YORK_TZ = TimeZone.getTimeZone("America/New_York");
-    private static final TimeZone LONDON_TZ = TimeZone.getTimeZone("Europe/London");
-    // A zone that matches LONDON_TZ for WHEN_NO_DST. It does not have DST so differs for WHEN_DST.
-    private static final TimeZone REYKJAVIK_TZ = TimeZone.getTimeZone("Atlantic/Reykjavik");
-    // Another zone that matches LONDON_TZ for WHEN_NO_DST. It does not have DST so differs for
+    // Zones used in the tests. NY_TZ and LON_TZ chosen because they never overlap but both have
+    // DST.
+    private static final TimeZone NY_TZ = TimeZone.getTimeZone("America/New_York");
+    private static final TimeZone LON_TZ = TimeZone.getTimeZone("Europe/London");
+    // A zone that matches LON_TZ for WHEN_NO_DST. It does not have DST so differs for WHEN_DST.
+    private static final TimeZone REYK_TZ = TimeZone.getTimeZone("Atlantic/Reykjavik");
+    // Another zone that matches LON_TZ for WHEN_NO_DST. It does not have DST so differs for
     // WHEN_DST.
     private static final TimeZone UTC_TZ = TimeZone.getTimeZone("Etc/UTC");
 
@@ -58,12 +61,12 @@
     // The offset applied to most zones during DST.
     private static final int NORMAL_DST_ADJUSTMENT = HOUR_MILLIS;
 
-    private static final int LONDON_NO_DST_OFFSET_MILLIS = 0;
-    private static final int LONDON_DST_OFFSET_MILLIS = LONDON_NO_DST_OFFSET_MILLIS
+    private static final int LON_NO_DST_TOTAL_OFFSET = 0;
+    private static final int LON_DST_TOTAL_OFFSET = LON_NO_DST_TOTAL_OFFSET
             + NORMAL_DST_ADJUSTMENT;
 
-    private static final int NEW_YORK_NO_DST_OFFSET_MILLIS = -5 * HOUR_MILLIS;
-    private static final int NEW_YORK_DST_OFFSET_MILLIS = NEW_YORK_NO_DST_OFFSET_MILLIS
+    private static final int NY_NO_DST_TOTAL_OFFSET = -5 * HOUR_MILLIS;
+    private static final int NY_DST_TOTAL_OFFSET = NY_NO_DST_TOTAL_OFFSET
             + NORMAL_DST_ADJUSTMENT;
 
     @Test
@@ -138,360 +141,136 @@
     }
 
     @Test
-    public void lookupByOffsetWithBiasDeprecated_oneCandidate() throws Exception {
+    public void lookupByOffsetWithBias_oneCandidate() throws Exception {
         CountryTimeZones countryTimeZones = CountryTimeZones.createValidated(
                 "gb", "Europe/London", true /* everUsesUtc */,
                 timeZoneMappings("Europe/London"), "test");
 
-        OffsetResult expectedResult = new OffsetResult(LONDON_TZ, true /* oneMatch */);
+        OffsetResult lonMatch = new OffsetResult(LON_TZ, true /* oneMatch */);
 
-        // The three parameters match the configured zone: offset, isDst and time.
-        assertOffsetResultEquals(expectedResult,
-                countryTimeZones.lookupByOffsetWithBias(LONDON_DST_OFFSET_MILLIS,
-                        true /* isDst */, WHEN_DST, null /* bias */));
-        assertOffsetResultEquals(expectedResult,
-                countryTimeZones.lookupByOffsetWithBias(LONDON_NO_DST_OFFSET_MILLIS,
-                        false /* isDst */, WHEN_NO_DST, null /* bias */));
+        // Placeholder constants to improve test case readability.
+        final Boolean isDst = true;
+        final Boolean notDst = false;
+        final Boolean unkIsDst = null;
+        final Integer goodDstOffset = HOUR_MILLIS; // Every DST used here is one hour ahead.
+        final Integer badDstOffset = HOUR_MILLIS + 1;
+        final Integer unkDstOffset = null;
+        final TimeZone noBias = null;
+        final OffsetResult noMatch = null;
 
-        // Some lookup failure cases where the offset, isDst and time do not match the configured
-        // zone.
-        OffsetResult noDstMatch1 = countryTimeZones.lookupByOffsetWithBias(
-                LONDON_DST_OFFSET_MILLIS, true /* isDst */, WHEN_NO_DST, null /* bias */);
-        assertNull(noDstMatch1);
+        Object[][] testCases = new Object[][] {
+                // totalOffsetMillis, isDst, dstOffsetMillis, whenMillis, bias, expectedMatch
 
-        OffsetResult noDstMatch2 = countryTimeZones.lookupByOffsetWithBias(
-                LONDON_DST_OFFSET_MILLIS, false /* isDst */, WHEN_NO_DST, null /* bias */);
-        assertNull(noDstMatch2);
+                // The parameters match the zone: total offset and time.
+                { LON_DST_TOTAL_OFFSET, unkIsDst, unkDstOffset, WHEN_DST, noBias, lonMatch },
+                { LON_NO_DST_TOTAL_OFFSET, unkIsDst, unkDstOffset, WHEN_NO_DST, noBias, lonMatch },
 
-        OffsetResult noDstMatch3 = countryTimeZones.lookupByOffsetWithBias(
-                LONDON_NO_DST_OFFSET_MILLIS, true /* isDst */, WHEN_DST, null /* bias */);
-        assertNull(noDstMatch3);
+                // The parameters match the zone: total offset, isDst and time.
+                { LON_DST_TOTAL_OFFSET, isDst, unkDstOffset, WHEN_DST, noBias, lonMatch },
+                { LON_DST_TOTAL_OFFSET, isDst, goodDstOffset, WHEN_DST, noBias, lonMatch },
+                { LON_NO_DST_TOTAL_OFFSET, notDst, unkDstOffset, WHEN_NO_DST, noBias, lonMatch },
 
-        OffsetResult noDstMatch4 = countryTimeZones.lookupByOffsetWithBias(
-                LONDON_NO_DST_OFFSET_MILLIS, true /* isDst */, WHEN_NO_DST, null /* bias */);
-        assertNull(noDstMatch4);
+                // Lookup failures: bad DST offset.
+                { LON_DST_TOTAL_OFFSET, isDst, badDstOffset, WHEN_DST, noBias, noMatch },
 
-        OffsetResult noDstMatch5 = countryTimeZones.lookupByOffsetWithBias(
-                LONDON_DST_OFFSET_MILLIS, false /* isDst */, WHEN_DST, null /* bias */);
-        assertNull(noDstMatch5);
+                // Some lookup failure cases where the total offset, isDst and time do not match the
+                // zone.
+                { LON_DST_TOTAL_OFFSET, isDst, unkDstOffset, WHEN_NO_DST, noBias, noMatch },
+                { LON_DST_TOTAL_OFFSET, notDst, unkDstOffset, WHEN_NO_DST, noBias, noMatch },
+                { LON_NO_DST_TOTAL_OFFSET, isDst, unkDstOffset, WHEN_DST, noBias, noMatch },
+                { LON_NO_DST_TOTAL_OFFSET, isDst, unkDstOffset, WHEN_NO_DST, noBias, noMatch },
+                { LON_DST_TOTAL_OFFSET, notDst, unkDstOffset, WHEN_DST, noBias, noMatch },
+                { LON_NO_DST_TOTAL_OFFSET, notDst, unkDstOffset, WHEN_DST, noBias, noMatch },
 
-        OffsetResult noDstMatch6 = countryTimeZones.lookupByOffsetWithBias(
-                LONDON_NO_DST_OFFSET_MILLIS, false /* isDst */, WHEN_DST, null /* bias */);
-        assertNull(noDstMatch6);
+                // Some bias cases below.
 
-        // Some bias cases below.
+                // The bias is irrelevant here: it matches what would be returned anyway.
+                { LON_DST_TOTAL_OFFSET, isDst, unkDstOffset, WHEN_DST, LON_TZ, lonMatch },
+                { LON_NO_DST_TOTAL_OFFSET, notDst, unkDstOffset, WHEN_NO_DST, LON_TZ, lonMatch },
 
-        // The bias is irrelevant here: it matches what would be returned anyway.
-        assertOffsetResultEquals(expectedResult,
-                countryTimeZones.lookupByOffsetWithBias(LONDON_DST_OFFSET_MILLIS,
-                        true /* isDst */, WHEN_DST, LONDON_TZ /* bias */));
-        assertOffsetResultEquals(expectedResult,
-                countryTimeZones.lookupByOffsetWithBias(LONDON_NO_DST_OFFSET_MILLIS,
-                        false /* isDst */, WHEN_NO_DST, LONDON_TZ /* bias */));
-        // A sample of a non-matching case with bias.
-        assertNull(countryTimeZones.lookupByOffsetWithBias(LONDON_DST_OFFSET_MILLIS,
-                true /* isDst */, WHEN_NO_DST, LONDON_TZ /* bias */));
+                // A sample of a non-matching case with bias.
+                { LON_DST_TOTAL_OFFSET, isDst, unkDstOffset, WHEN_NO_DST, LON_TZ, noMatch },
 
-        // The bias should be ignored: it doesn't match any of the country's zones.
-        assertOffsetResultEquals(expectedResult,
-                countryTimeZones.lookupByOffsetWithBias(LONDON_DST_OFFSET_MILLIS,
-                        true /* isDst */, WHEN_DST, NEW_YORK_TZ /* bias */));
+                // The bias should be ignored: it doesn't match any of the country's zones.
+                { LON_DST_TOTAL_OFFSET, isDst, unkDstOffset, WHEN_DST, NY_TZ, lonMatch },
 
-        // The bias should still be ignored even though it matches the offset information given:
-        // it doesn't match any of the country's configured zones.
-        assertNull(countryTimeZones.lookupByOffsetWithBias(NEW_YORK_DST_OFFSET_MILLIS,
-                true /* isDst */, WHEN_DST, NEW_YORK_TZ /* bias */));
+                // The bias should still be ignored even though it matches the offset information
+                // given it doesn't match any of the country's zones.
+                { NY_DST_TOTAL_OFFSET, isDst, unkDstOffset, WHEN_DST, NY_TZ, noMatch },
+        };
+        executeLookupByOffsetWithBiasTestCases(countryTimeZones, testCases);
     }
 
     @Test
-    public void lookupByOffsetWithBiasDeprecated_multipleNonOverlappingCandidates()
-            throws Exception {
+    public void lookupByOffsetWithBias_multipleNonOverlappingCandidates() throws Exception {
         CountryTimeZones countryTimeZones = CountryTimeZones.createValidated(
                 "xx", "Europe/London", true /* everUsesUtc */,
                 timeZoneMappings("America/New_York", "Europe/London"), "test");
 
-        OffsetResult expectedLondonResult = new OffsetResult(LONDON_TZ, true /* oneMatch */);
-        OffsetResult expectedNewYorkResult = new OffsetResult(NEW_YORK_TZ, true /* oneMatch */);
+        OffsetResult lonMatch = new OffsetResult(LON_TZ, true /* oneMatch */);
+        OffsetResult nyMatch = new OffsetResult(NY_TZ, true /* oneMatch */);
 
-        // The three parameters match the configured zone: offset, isDst and time.
-        assertOffsetResultEquals(expectedLondonResult, countryTimeZones.lookupByOffsetWithBias(
-                LONDON_DST_OFFSET_MILLIS, true /* isDst */, WHEN_DST, null /* bias */));
-        assertOffsetResultEquals(expectedLondonResult, countryTimeZones.lookupByOffsetWithBias(
-                LONDON_NO_DST_OFFSET_MILLIS, false /* isDst */, WHEN_NO_DST, null /* bias */));
-        assertOffsetResultEquals(expectedNewYorkResult, countryTimeZones.lookupByOffsetWithBias(
-                NEW_YORK_DST_OFFSET_MILLIS, true /* isDst */, WHEN_DST, null /* bias */));
-        assertOffsetResultEquals(expectedNewYorkResult, countryTimeZones.lookupByOffsetWithBias(
-                NEW_YORK_NO_DST_OFFSET_MILLIS, false /* isDst */, WHEN_NO_DST, null /* bias */));
+        // Placeholder constants to improve test case readability.
+        final Boolean isDst = true;
+        final Boolean notDst = false;
+        final Boolean unkIsDst = null;
+        final Integer unkDstOffset = null;
+        final Integer goodDstOffset = HOUR_MILLIS; // Every DST used here is one hour ahead.
+        final Integer badDstOffset = HOUR_MILLIS + 1;
+        final TimeZone noBias = null;
+        final OffsetResult noMatch = null;
 
-        // Some lookup failure cases where the offset, isDst and time do not match the configured
-        // zone. This is a sample, not complete.
-        OffsetResult noDstMatch1 = countryTimeZones.lookupByOffsetWithBias(
-                LONDON_DST_OFFSET_MILLIS, true /* isDst */, WHEN_NO_DST, null /* bias */);
-        assertNull(noDstMatch1);
+        Object[][] testCases = new Object[][] {
+                // totalOffsetMillis, isDst, dstOffsetMillis, whenMillis, bias, expectedMatch
 
-        OffsetResult noDstMatch2 = countryTimeZones.lookupByOffsetWithBias(
-                LONDON_DST_OFFSET_MILLIS, false /* isDst */, WHEN_NO_DST, null /* bias */);
-        assertNull(noDstMatch2);
+                // The parameters match the zone: total offset and time.
+                { LON_DST_TOTAL_OFFSET, unkIsDst, unkDstOffset, WHEN_DST, noBias, lonMatch },
+                { LON_NO_DST_TOTAL_OFFSET, unkIsDst, unkDstOffset, WHEN_NO_DST, noBias, lonMatch },
+                { NY_NO_DST_TOTAL_OFFSET, unkIsDst, unkDstOffset, WHEN_NO_DST, noBias, nyMatch },
+                { NY_DST_TOTAL_OFFSET, unkIsDst, unkDstOffset, WHEN_DST, noBias, nyMatch },
 
-        OffsetResult noDstMatch3 = countryTimeZones.lookupByOffsetWithBias(
-                NEW_YORK_NO_DST_OFFSET_MILLIS, true /* isDst */, WHEN_DST, null /* bias */);
-        assertNull(noDstMatch3);
+                // The parameters match the zone: total offset, isDst and time.
+                { LON_DST_TOTAL_OFFSET, isDst, unkDstOffset, WHEN_DST, noBias, lonMatch },
+                { LON_NO_DST_TOTAL_OFFSET, notDst, unkDstOffset, WHEN_NO_DST, noBias, lonMatch },
+                { NY_DST_TOTAL_OFFSET, isDst, unkDstOffset, WHEN_DST, noBias, nyMatch },
+                { NY_NO_DST_TOTAL_OFFSET, notDst, unkDstOffset, WHEN_NO_DST, noBias, nyMatch },
 
-        OffsetResult noDstMatch4 = countryTimeZones.lookupByOffsetWithBias(
-                NEW_YORK_NO_DST_OFFSET_MILLIS, true /* isDst */, WHEN_NO_DST, null /* bias */);
-        assertNull(noDstMatch4);
+                // The parameters match the zone: total offset, isDst, DST offset and time.
+                { LON_DST_TOTAL_OFFSET, isDst, goodDstOffset, WHEN_DST, noBias, lonMatch },
+                { NY_DST_TOTAL_OFFSET, isDst, goodDstOffset, WHEN_DST, noBias, nyMatch },
 
-        OffsetResult noDstMatch5 = countryTimeZones.lookupByOffsetWithBias(
-                LONDON_DST_OFFSET_MILLIS, false /* isDst */, WHEN_DST, null /* bias */);
-        assertNull(noDstMatch5);
+                // Lookup failures: bad DST offset.
+                { LON_DST_TOTAL_OFFSET, isDst, badDstOffset, WHEN_DST, noBias, noMatch },
+                { NY_DST_TOTAL_OFFSET, isDst, badDstOffset, WHEN_DST, noBias, noMatch },
 
-        OffsetResult noDstMatch6 = countryTimeZones.lookupByOffsetWithBias(
-                LONDON_NO_DST_OFFSET_MILLIS, false /* isDst */, WHEN_DST, null /* bias */);
-        assertNull(noDstMatch6);
+                // Some lookup failure cases where the total offset, isDst and time do not match the
+                // zone. This is a sample, not complete.
+                { LON_DST_TOTAL_OFFSET, isDst, unkDstOffset, WHEN_NO_DST, noBias, noMatch },
+                { LON_DST_TOTAL_OFFSET, unkIsDst, unkDstOffset, WHEN_NO_DST, noBias, noMatch },
+                { LON_DST_TOTAL_OFFSET, notDst, unkDstOffset, WHEN_DST, noBias, noMatch },
+                { LON_NO_DST_TOTAL_OFFSET, isDst, unkDstOffset, WHEN_DST, noBias, noMatch },
+                { LON_NO_DST_TOTAL_OFFSET, isDst, unkDstOffset, WHEN_NO_DST, noBias, noMatch },
+                { LON_NO_DST_TOTAL_OFFSET, unkIsDst, unkDstOffset, WHEN_DST, noBias, noMatch },
+                { LON_NO_DST_TOTAL_OFFSET, notDst, unkDstOffset, WHEN_DST, noBias, noMatch },
 
-        // Some bias cases below.
+                // Some bias cases below.
 
-        // The bias is irrelevant here: it matches what would be returned anyway.
-        assertOffsetResultEquals(expectedLondonResult, countryTimeZones.lookupByOffsetWithBias(
-                LONDON_DST_OFFSET_MILLIS, true /* isDst */, WHEN_DST, LONDON_TZ /* bias */));
-        assertOffsetResultEquals(expectedLondonResult, countryTimeZones.lookupByOffsetWithBias(
-                LONDON_NO_DST_OFFSET_MILLIS, false /* isDst */, WHEN_NO_DST, LONDON_TZ /* bias */));
-        // A sample of a non-matching case with bias.
-        assertNull(countryTimeZones.lookupByOffsetWithBias(
-                LONDON_DST_OFFSET_MILLIS, true /* isDst */, WHEN_NO_DST, LONDON_TZ /* bias */));
+                // The bias is irrelevant here: it matches what would be returned anyway.
+                { LON_DST_TOTAL_OFFSET, isDst, unkDstOffset, WHEN_DST, LON_TZ, lonMatch },
+                { LON_DST_TOTAL_OFFSET, unkIsDst, unkDstOffset, WHEN_DST, LON_TZ, lonMatch },
+                { LON_NO_DST_TOTAL_OFFSET, unkIsDst, unkDstOffset, WHEN_NO_DST, LON_TZ, lonMatch },
 
-        // The bias should be ignored: it matches a configured zone, but the offset is wrong so
-        // should not be considered a match.
-        assertOffsetResultEquals(expectedLondonResult, countryTimeZones.lookupByOffsetWithBias(
-                LONDON_DST_OFFSET_MILLIS, true /* isDst */, WHEN_DST, NEW_YORK_TZ /* bias */));
-    }
+                // A sample of non-matching cases with bias.
+                { LON_NO_DST_TOTAL_OFFSET, isDst, unkDstOffset, WHEN_NO_DST, LON_TZ, noMatch },
+                { LON_DST_TOTAL_OFFSET, isDst, unkDstOffset, WHEN_NO_DST, LON_TZ, noMatch },
+                { LON_DST_TOTAL_OFFSET, unkIsDst, unkDstOffset, WHEN_NO_DST, LON_TZ, noMatch },
 
-    // This is an artificial case very similar to America/Denver and America/Phoenix in the US: both
-    // have the same offset for 6 months of the year but diverge. Australia/Lord_Howe too.
-    @Test
-    public void lookupByOffsetWithBiasDeprecated_multipleOverlappingCandidates() throws Exception {
-        // Three zones that have the same offset for some of the year. Europe/London changes
-        // offset WHEN_DST, the others do not.
-        CountryTimeZones countryTimeZones = CountryTimeZones.createValidated(
-                "xx", "Europe/London", true /* everUsesUtc */,
-                timeZoneMappings("Atlantic/Reykjavik", "Europe/London", "Etc/UTC"), "test");
-
-        // This is the no-DST offset for LONDON_TZ, REYKJAVIK_TZ. UTC_TZ.
-        final int noDstOffset = LONDON_NO_DST_OFFSET_MILLIS;
-        // This is the DST offset for LONDON_TZ.
-        final int dstOffset = LONDON_DST_OFFSET_MILLIS;
-
-        OffsetResult expectedLondonOnlyMatch = new OffsetResult(LONDON_TZ, true /* oneMatch */);
-        OffsetResult expectedReykjavikBestMatch =
-                new OffsetResult(REYKJAVIK_TZ, false /* oneMatch */);
-
-        // The three parameters match the configured zone: offset, isDst and when.
-        assertOffsetResultEquals(expectedLondonOnlyMatch,
-                countryTimeZones.lookupByOffsetWithBias(dstOffset, true /* isDst */, WHEN_DST,
-                        null /* bias */));
-        assertOffsetResultEquals(expectedReykjavikBestMatch,
-                countryTimeZones.lookupByOffsetWithBias(noDstOffset, false /* isDst */, WHEN_NO_DST,
-                        null /* bias */));
-        assertOffsetResultEquals(expectedLondonOnlyMatch,
-                countryTimeZones.lookupByOffsetWithBias(dstOffset, true /* isDst */, WHEN_DST,
-                        null /* bias */));
-        assertOffsetResultEquals(expectedReykjavikBestMatch,
-                countryTimeZones.lookupByOffsetWithBias(noDstOffset, false /* isDst */, WHEN_NO_DST,
-                        null /* bias */));
-        assertOffsetResultEquals(expectedReykjavikBestMatch,
-                countryTimeZones.lookupByOffsetWithBias(noDstOffset, false /* isDst */, WHEN_DST,
-                        null /* bias */));
-
-        // Some lookup failure cases where the offset, isDst and time do not match the configured
-        // zones.
-        OffsetResult noDstMatch1 = countryTimeZones.lookupByOffsetWithBias(dstOffset,
-                true /* isDst */, WHEN_NO_DST, null /* bias */);
-        assertNull(noDstMatch1);
-
-        OffsetResult noDstMatch2 = countryTimeZones.lookupByOffsetWithBias(noDstOffset,
-                true /* isDst */, WHEN_DST, null /* bias */);
-        assertNull(noDstMatch2);
-
-        OffsetResult noDstMatch3 = countryTimeZones.lookupByOffsetWithBias(noDstOffset,
-                true /* isDst */, WHEN_NO_DST, null /* bias */);
-        assertNull(noDstMatch3);
-
-        OffsetResult noDstMatch4 = countryTimeZones.lookupByOffsetWithBias(dstOffset,
-                false /* isDst */, WHEN_DST, null /* bias */);
-        assertNull(noDstMatch4);
-
-
-        // Some bias cases below.
-
-        // Multiple zones match but Reykjavik is the bias.
-        assertOffsetResultEquals(expectedReykjavikBestMatch,
-                countryTimeZones.lookupByOffsetWithBias(noDstOffset, false /* isDst */, WHEN_NO_DST,
-                        REYKJAVIK_TZ /* bias */));
-
-        // Multiple zones match but London is the bias.
-        OffsetResult expectedLondonBestMatch = new OffsetResult(LONDON_TZ, false /* oneMatch */);
-        assertOffsetResultEquals(expectedLondonBestMatch,
-                countryTimeZones.lookupByOffsetWithBias(noDstOffset, false /* isDst */, WHEN_NO_DST,
-                        LONDON_TZ /* bias */));
-
-        // Multiple zones match but UTC is the bias.
-        OffsetResult expectedUtcResult = new OffsetResult(UTC_TZ, false /* oneMatch */);
-        assertOffsetResultEquals(expectedUtcResult,
-                countryTimeZones.lookupByOffsetWithBias(noDstOffset, false /* isDst */, WHEN_NO_DST,
-                        UTC_TZ /* bias */));
-
-        // The bias should be ignored: it matches a configured zone, but the offset is wrong so
-        // should not be considered a match.
-        assertOffsetResultEquals(expectedLondonOnlyMatch,
-                countryTimeZones.lookupByOffsetWithBias(LONDON_DST_OFFSET_MILLIS, true /* isDst */,
-                        WHEN_DST, REYKJAVIK_TZ /* bias */));
-    }
-
-    @Test
-    public void lookupByOffsetWithBias_oneCandidate() throws Exception {
-        CountryTimeZones countryTimeZones = CountryTimeZones.createValidated(
-                "gb", "Europe/London", true /* uses UTC */, timeZoneMappings("Europe/London"),
-                "test");
-
-        OffsetResult expectedResult = new OffsetResult(LONDON_TZ, true /* oneMatch */);
-
-        // The three parameters match the configured zone: offset, isDst and time.
-        assertOffsetResultEquals(expectedResult,
-                countryTimeZones.lookupByOffsetWithBias(LONDON_DST_OFFSET_MILLIS,
-                        NORMAL_DST_ADJUSTMENT, WHEN_DST, null /* bias */));
-        assertOffsetResultEquals(expectedResult,
-                countryTimeZones.lookupByOffsetWithBias(LONDON_NO_DST_OFFSET_MILLIS,
-                        0 /* no DST */, WHEN_NO_DST, null /* bias */));
-        assertOffsetResultEquals(expectedResult,
-                countryTimeZones.lookupByOffsetWithBias(LONDON_DST_OFFSET_MILLIS,
-                        null /* unknown DST */, WHEN_DST, null /* bias */));
-        assertOffsetResultEquals(expectedResult,
-                countryTimeZones.lookupByOffsetWithBias(LONDON_NO_DST_OFFSET_MILLIS,
-                        null /* unknown DST */, WHEN_NO_DST, null /* bias */));
-
-        // Some lookup failure cases where the offset, DST offset and time do not match the
-        // configured zone.
-        OffsetResult noDstMatch1 = countryTimeZones.lookupByOffsetWithBias(
-                LONDON_DST_OFFSET_MILLIS, NORMAL_DST_ADJUSTMENT, WHEN_NO_DST, null /* bias */);
-        assertNull(noDstMatch1);
-
-        OffsetResult noDstMatch2 = countryTimeZones.lookupByOffsetWithBias(
-                LONDON_DST_OFFSET_MILLIS, 0 /* no DST */, WHEN_NO_DST, null /* bias */);
-        assertNull(noDstMatch2);
-
-        OffsetResult noDstMatch3 = countryTimeZones.lookupByOffsetWithBias(
-                LONDON_NO_DST_OFFSET_MILLIS, NORMAL_DST_ADJUSTMENT, WHEN_DST, null /* bias */);
-        assertNull(noDstMatch3);
-
-        OffsetResult noDstMatch4 = countryTimeZones.lookupByOffsetWithBias(
-                LONDON_NO_DST_OFFSET_MILLIS, NORMAL_DST_ADJUSTMENT, WHEN_NO_DST, null /* bias */);
-        assertNull(noDstMatch4);
-
-        OffsetResult noDstMatch5 = countryTimeZones.lookupByOffsetWithBias(
-                LONDON_DST_OFFSET_MILLIS, 0 /* no DST */, WHEN_DST, null /* bias */);
-        assertNull(noDstMatch5);
-
-        OffsetResult noDstMatch6 = countryTimeZones.lookupByOffsetWithBias(
-                LONDON_NO_DST_OFFSET_MILLIS, 0 /* no DST */, WHEN_DST, null /* bias */);
-        assertNull(noDstMatch6);
-
-        // Some bias cases below.
-
-        // The bias is irrelevant here: it matches what would be returned anyway.
-        assertOffsetResultEquals(expectedResult,
-                countryTimeZones.lookupByOffsetWithBias(LONDON_DST_OFFSET_MILLIS,
-                        NORMAL_DST_ADJUSTMENT, WHEN_DST, LONDON_TZ /* bias */));
-        assertOffsetResultEquals(expectedResult,
-                countryTimeZones.lookupByOffsetWithBias(LONDON_NO_DST_OFFSET_MILLIS,
-                        0 /* no DST */, WHEN_NO_DST, LONDON_TZ /* bias */));
-        assertOffsetResultEquals(expectedResult,
-                countryTimeZones.lookupByOffsetWithBias(LONDON_NO_DST_OFFSET_MILLIS,
-                        null /* unknown DST */, WHEN_NO_DST, LONDON_TZ /* bias */));
-        // A sample of a non-matching case with bias.
-        assertNull(countryTimeZones.lookupByOffsetWithBias(LONDON_DST_OFFSET_MILLIS,
-                NORMAL_DST_ADJUSTMENT, WHEN_NO_DST, LONDON_TZ /* bias */));
-
-        // The bias should be ignored: it doesn't match any of the country's zones.
-        assertOffsetResultEquals(expectedResult,
-                countryTimeZones.lookupByOffsetWithBias(LONDON_DST_OFFSET_MILLIS,
-                        NORMAL_DST_ADJUSTMENT, WHEN_DST, NEW_YORK_TZ /* bias */));
-
-        // The bias should still be ignored even though it matches the offset information given:
-        // it doesn't match any of the country's configured zones.
-        assertNull(countryTimeZones.lookupByOffsetWithBias(NEW_YORK_DST_OFFSET_MILLIS,
-                NORMAL_DST_ADJUSTMENT, WHEN_DST, NEW_YORK_TZ /* bias */));
-    }
-
-    @Test
-    public void lookupByOffsetWithBias_multipleNonOverlappingCandidates()
-            throws Exception {
-        CountryTimeZones countryTimeZones = CountryTimeZones.createValidated(
-                "xx", "Europe/London", true /* uses UTC */,
-                timeZoneMappings("America/New_York", "Europe/London"), "test");
-
-        OffsetResult expectedLondonResult = new OffsetResult(LONDON_TZ, true /* oneMatch */);
-        OffsetResult expectedNewYorkResult = new OffsetResult(NEW_YORK_TZ, true /* oneMatch */);
-
-        // The three parameters match the configured zone: offset, DST offset and time.
-        assertOffsetResultEquals(expectedLondonResult, countryTimeZones.lookupByOffsetWithBias(
-                LONDON_DST_OFFSET_MILLIS, NORMAL_DST_ADJUSTMENT, WHEN_DST, null /* bias */));
-        assertOffsetResultEquals(expectedLondonResult, countryTimeZones.lookupByOffsetWithBias(
-                LONDON_NO_DST_OFFSET_MILLIS, 0 /* no DST */, WHEN_NO_DST, null /* bias */));
-        assertOffsetResultEquals(expectedLondonResult, countryTimeZones.lookupByOffsetWithBias(
-                LONDON_NO_DST_OFFSET_MILLIS, null /* unknown DST */, WHEN_NO_DST, null /* bias */));
-        assertOffsetResultEquals(expectedNewYorkResult, countryTimeZones.lookupByOffsetWithBias(
-                NEW_YORK_DST_OFFSET_MILLIS, NORMAL_DST_ADJUSTMENT, WHEN_DST, null /* bias */));
-        assertOffsetResultEquals(expectedNewYorkResult, countryTimeZones.lookupByOffsetWithBias(
-                NEW_YORK_NO_DST_OFFSET_MILLIS, 0 /* no DST */, WHEN_NO_DST, null /* bias */));
-        assertOffsetResultEquals(expectedNewYorkResult, countryTimeZones.lookupByOffsetWithBias(
-                NEW_YORK_NO_DST_OFFSET_MILLIS, null /* unknown DST */, WHEN_NO_DST,
-                null /* bias */));
-
-        // Some lookup failure cases where the offset, DST offset and time do not match the
-        // configured zone. This is a sample, not complete.
-        OffsetResult noDstMatch1 = countryTimeZones.lookupByOffsetWithBias(
-                LONDON_DST_OFFSET_MILLIS, NORMAL_DST_ADJUSTMENT, WHEN_NO_DST, null /* bias */);
-        assertNull(noDstMatch1);
-
-        OffsetResult noDstMatch2 = countryTimeZones.lookupByOffsetWithBias(
-                LONDON_DST_OFFSET_MILLIS, 0 /* no DST */, WHEN_NO_DST, null /* bias */);
-        assertNull(noDstMatch2);
-
-        OffsetResult noDstMatch3 = countryTimeZones.lookupByOffsetWithBias(
-                NEW_YORK_NO_DST_OFFSET_MILLIS, NORMAL_DST_ADJUSTMENT, WHEN_DST, null /* bias */);
-        assertNull(noDstMatch3);
-
-        OffsetResult noDstMatch4 = countryTimeZones.lookupByOffsetWithBias(
-                NEW_YORK_NO_DST_OFFSET_MILLIS, NORMAL_DST_ADJUSTMENT, WHEN_NO_DST, null /* bias */);
-        assertNull(noDstMatch4);
-
-        OffsetResult noDstMatch5 = countryTimeZones.lookupByOffsetWithBias(
-                LONDON_DST_OFFSET_MILLIS, 0 /* no DST */, WHEN_DST, null /* bias */);
-        assertNull(noDstMatch5);
-
-        OffsetResult noDstMatch6 = countryTimeZones.lookupByOffsetWithBias(
-                LONDON_NO_DST_OFFSET_MILLIS, 0 /* no DST */, WHEN_DST, null /* bias */);
-        assertNull(noDstMatch6);
-
-        // Some bias cases below.
-
-        // The bias is irrelevant here: it matches what would be returned anyway.
-        assertOffsetResultEquals(expectedLondonResult, countryTimeZones.lookupByOffsetWithBias(
-                LONDON_DST_OFFSET_MILLIS, NORMAL_DST_ADJUSTMENT, WHEN_DST, LONDON_TZ /* bias */));
-        assertOffsetResultEquals(expectedLondonResult, countryTimeZones.lookupByOffsetWithBias(
-                LONDON_NO_DST_OFFSET_MILLIS, 0 /* no DST */, WHEN_NO_DST, LONDON_TZ /* bias */));
-        assertOffsetResultEquals(expectedLondonResult, countryTimeZones.lookupByOffsetWithBias(
-                LONDON_NO_DST_OFFSET_MILLIS, null /* unknown DST */, WHEN_NO_DST,
-                LONDON_TZ /* bias */));
-
-        // A sample of a non-matching case with bias.
-        assertNull(countryTimeZones.lookupByOffsetWithBias(
-                LONDON_DST_OFFSET_MILLIS, NORMAL_DST_ADJUSTMENT, WHEN_NO_DST, LONDON_TZ /* bias */));
-
-        // The bias should be ignored: it matches a configured zone, but the offset is wrong so
-        // should not be considered a match.
-        assertOffsetResultEquals(expectedLondonResult, countryTimeZones.lookupByOffsetWithBias(
-                LONDON_DST_OFFSET_MILLIS, NORMAL_DST_ADJUSTMENT, WHEN_DST, NEW_YORK_TZ /* bias */));
+                // The bias should be ignored: it matches a zone, but the offset is wrong so
+                // should not be considered a match.
+                { LON_DST_TOTAL_OFFSET, isDst, unkDstOffset, WHEN_DST, NY_TZ, lonMatch },
+                { LON_DST_TOTAL_OFFSET, unkIsDst, unkDstOffset, WHEN_DST, NY_TZ, lonMatch },
+        };
+        executeLookupByOffsetWithBiasTestCases(countryTimeZones, testCases);
     }
 
     // This is an artificial case very similar to America/Denver and America/Phoenix in the US: both
@@ -501,100 +280,116 @@
         // Three zones that have the same offset for some of the year. Europe/London changes
         // offset WHEN_DST, the others do not.
         CountryTimeZones countryTimeZones = CountryTimeZones.createValidated(
-                "xx", "Europe/London", true /* uses UTC */,
+                "xx", "Europe/London", true /* everUsesUtc */,
                 timeZoneMappings("Atlantic/Reykjavik", "Europe/London", "Etc/UTC"), "test");
 
-        // This is the no-DST offset for LONDON_TZ, REYKJAVIK_TZ. UTC_TZ.
-        final int noDstOffset = LONDON_NO_DST_OFFSET_MILLIS;
-        // This is the DST offset for LONDON_TZ.
-        final int dstOffset = LONDON_DST_OFFSET_MILLIS;
+        // Placeholder constants to improve test case readability.
+        final Boolean isDst = true;
+        final Boolean notDst = false;
+        final Boolean unkIsDst = null;
+        final Integer unkDstOffset = null;
+        final Integer goodDstOffset = HOUR_MILLIS; // Every DST used here is one hour ahead.
+        final Integer badDstOffset = HOUR_MILLIS + 1;
+        final TimeZone noBias = null;
+        final OffsetResult noMatch = null;
 
-        OffsetResult expectedLondonOnlyMatch = new OffsetResult(LONDON_TZ, true /* oneMatch */);
-        OffsetResult expectedReykjavikBestMatch =
-                new OffsetResult(REYKJAVIK_TZ, false /* oneMatch */);
+        // This is the no-DST offset for LON_TZ, REYK_TZ. UTC_TZ.
+        final int noDstTotalOffset = LON_NO_DST_TOTAL_OFFSET;
+        // This is the DST offset for LON_TZ.
+        final int dstTotalOffset = LON_DST_TOTAL_OFFSET;
 
-        // The three parameters match the configured zone: offset, DST offset and time.
-        assertOffsetResultEquals(expectedLondonOnlyMatch,
-                countryTimeZones.lookupByOffsetWithBias(dstOffset, NORMAL_DST_ADJUSTMENT, WHEN_DST,
-                        null /* bias */));
-        assertOffsetResultEquals(expectedReykjavikBestMatch,
-                countryTimeZones.lookupByOffsetWithBias(noDstOffset, 0 /* no DST */, WHEN_NO_DST,
-                        null /* bias */));
-        assertOffsetResultEquals(expectedLondonOnlyMatch,
-                countryTimeZones.lookupByOffsetWithBias(dstOffset, NORMAL_DST_ADJUSTMENT, WHEN_DST,
-                        null /* bias */));
-        assertOffsetResultEquals(expectedReykjavikBestMatch,
-                countryTimeZones.lookupByOffsetWithBias(noDstOffset, 0 /* no DST */, WHEN_NO_DST,
-                        null /* bias */));
-        assertOffsetResultEquals(expectedReykjavikBestMatch,
-                countryTimeZones.lookupByOffsetWithBias(noDstOffset, 0 /* no DST */, WHEN_DST,
-                        null /* bias */));
+        OffsetResult lonOnlyMatch = new OffsetResult(LON_TZ, true /* oneMatch */);
+        OffsetResult lonBestMatch = new OffsetResult(LON_TZ, false /* oneMatch */);
+        OffsetResult reykBestMatch = new OffsetResult(REYK_TZ, false /* oneMatch */);
+        OffsetResult utcBestMatch = new OffsetResult(UTC_TZ, false /* oneMatch */);
 
-        // Unknown DST cases
-        assertOffsetResultEquals(expectedReykjavikBestMatch,
-                countryTimeZones.lookupByOffsetWithBias(noDstOffset, null, WHEN_NO_DST,
-                        null /* bias */));
-        assertOffsetResultEquals(expectedReykjavikBestMatch,
-                countryTimeZones.lookupByOffsetWithBias(noDstOffset, null, WHEN_DST,
-                        null /* bias */));
-        assertNull(countryTimeZones.lookupByOffsetWithBias(dstOffset, null, WHEN_NO_DST,
-                        null /* bias */));
-        assertOffsetResultEquals(expectedLondonOnlyMatch,
-                countryTimeZones.lookupByOffsetWithBias(dstOffset, null, WHEN_DST,
-                        null /* bias */));
+        Object[][] testCases = new Object[][] {
+                // totalOffsetMillis, isDst, dstOffsetMillis, whenMillis, bias, expectedMatch
 
-        // Some lookup failure cases where the offset, DST offset and time do not match the
-        // configured zones.
-        OffsetResult noDstMatch1 = countryTimeZones.lookupByOffsetWithBias(dstOffset,
-                NORMAL_DST_ADJUSTMENT, WHEN_NO_DST, null /* bias */);
-        assertNull(noDstMatch1);
+                // The parameters match one zone: total offset and time.
+                { dstTotalOffset, unkIsDst, unkDstOffset, WHEN_DST, noBias, lonOnlyMatch },
+                { dstTotalOffset, unkIsDst, unkDstOffset, WHEN_DST, noBias, lonOnlyMatch },
 
-        OffsetResult noDstMatch2 = countryTimeZones.lookupByOffsetWithBias(noDstOffset,
-                NORMAL_DST_ADJUSTMENT, WHEN_DST, null /* bias */);
-        assertNull(noDstMatch2);
+                // The parameters match several zones: total offset and time.
+                { noDstTotalOffset, unkIsDst, unkDstOffset, WHEN_NO_DST, noBias, reykBestMatch },
+                { noDstTotalOffset, unkIsDst, unkDstOffset, WHEN_DST, noBias, reykBestMatch },
 
-        OffsetResult noDstMatch3 = countryTimeZones.lookupByOffsetWithBias(noDstOffset,
-                NORMAL_DST_ADJUSTMENT, WHEN_NO_DST, null /* bias */);
-        assertNull(noDstMatch3);
+                // The parameters match one zone: total offset, isDst and time.
+                { dstTotalOffset, isDst, unkDstOffset, WHEN_DST, noBias, lonOnlyMatch },
+                { dstTotalOffset, isDst, unkDstOffset, WHEN_DST, noBias, lonOnlyMatch },
 
-        OffsetResult noDstMatch4 = countryTimeZones.lookupByOffsetWithBias(dstOffset,
-                0 /* no DST */, WHEN_DST, null /* bias */);
-        assertNull(noDstMatch4);
+                // The parameters match one zone: total offset, isDst, DST offset and time.
+                { dstTotalOffset, isDst, goodDstOffset, WHEN_DST, noBias, lonOnlyMatch },
 
+                // The parameters match several zones: total offset, isDst and time.
+                { noDstTotalOffset, notDst, unkDstOffset, WHEN_NO_DST, noBias, reykBestMatch },
+                { noDstTotalOffset, notDst, unkDstOffset, WHEN_DST, noBias, reykBestMatch },
 
-        // Some bias cases below.
+                // Lookup failures: bad DST offset.
+                { dstTotalOffset, isDst, badDstOffset, WHEN_DST, noBias, noMatch },
 
-        // Multiple zones match but Reykjavik is the bias.
-        assertOffsetResultEquals(expectedReykjavikBestMatch,
-                countryTimeZones.lookupByOffsetWithBias(noDstOffset, 0 /* no DST */, WHEN_NO_DST,
-                        REYKJAVIK_TZ /* bias */));
-        assertOffsetResultEquals(expectedReykjavikBestMatch,
-                countryTimeZones.lookupByOffsetWithBias(noDstOffset, null /* unknown DST */,
-                        WHEN_NO_DST, REYKJAVIK_TZ /* bias */));
+                // Some lookup failure cases where the total offset, isDst and time do not match any
+                // zone.
+                { dstTotalOffset, isDst, unkDstOffset, WHEN_NO_DST, noBias, noMatch },
+                { dstTotalOffset, unkIsDst, unkDstOffset, WHEN_NO_DST, noBias, noMatch },
+                { noDstTotalOffset, isDst, unkDstOffset, WHEN_NO_DST, noBias, noMatch },
+                { noDstTotalOffset, isDst, unkDstOffset, WHEN_DST, noBias, noMatch },
 
-        // Multiple zones match but London is the bias.
-        OffsetResult expectedLondonBestMatch = new OffsetResult(LONDON_TZ, false /* oneMatch */);
-        assertOffsetResultEquals(expectedLondonBestMatch,
-                countryTimeZones.lookupByOffsetWithBias(noDstOffset, 0 /* no DST */, WHEN_NO_DST,
-                        LONDON_TZ /* bias */));
-        assertOffsetResultEquals(expectedLondonBestMatch,
-                countryTimeZones.lookupByOffsetWithBias(noDstOffset, null /* unknown DST */,
-                        WHEN_NO_DST, LONDON_TZ /* bias */));
+                // Some bias cases below.
 
-        // Multiple zones match but UTC is the bias.
-        OffsetResult expectedUtcResult = new OffsetResult(UTC_TZ, false /* oneMatch */);
-        assertOffsetResultEquals(expectedUtcResult,
-                countryTimeZones.lookupByOffsetWithBias(noDstOffset, 0 /* no DST */, WHEN_NO_DST,
-                        UTC_TZ /* bias */));
-        assertOffsetResultEquals(expectedUtcResult,
-                countryTimeZones.lookupByOffsetWithBias(noDstOffset, null /* unknown DST */,
-                        WHEN_NO_DST, UTC_TZ /* bias */));
+                // Multiple zones match but Reykjavik is the bias.
+                { noDstTotalOffset, notDst, unkDstOffset, WHEN_NO_DST, REYK_TZ, reykBestMatch },
 
-        // The bias should be ignored: it matches a configured zone, but the offset is wrong so
-        // should not be considered a match.
-        assertOffsetResultEquals(expectedLondonOnlyMatch,
-                countryTimeZones.lookupByOffsetWithBias(LONDON_DST_OFFSET_MILLIS,
-                        NORMAL_DST_ADJUSTMENT, WHEN_DST, REYKJAVIK_TZ /* bias */));
+                // Multiple zones match but London is the bias.
+                { noDstTotalOffset, notDst, unkDstOffset, WHEN_NO_DST, LON_TZ, lonBestMatch },
+
+                // Multiple zones match but UTC is the bias.
+                { noDstTotalOffset, notDst, unkDstOffset, WHEN_NO_DST, UTC_TZ, utcBestMatch },
+
+                // The bias should be ignored: it matches a zone, but the offset is wrong so
+                // should not be considered a match.
+                { LON_DST_TOTAL_OFFSET, isDst, unkDstOffset, WHEN_DST, REYK_TZ, lonOnlyMatch },
+                { LON_DST_TOTAL_OFFSET, unkIsDst, unkDstOffset, WHEN_DST, REYK_TZ, lonOnlyMatch },
+        };
+        executeLookupByOffsetWithBiasTestCases(countryTimeZones, testCases);
+    }
+
+    private static void executeLookupByOffsetWithBiasTestCases(
+            CountryTimeZones countryTimeZones, Object[][] testCases) {
+
+        List<String> failures = new ArrayList<>();
+        for (int i = 0; i < testCases.length; i++) {
+            Object[] testCase = testCases[i];
+            int totalOffsetMillis = (int) testCase[0];
+            Boolean isDst = (Boolean) testCase[1];
+            Integer dstOffsetMillis = (Integer) testCase[2];
+            long whenMillis = (Long) testCase[3];
+            TimeZone bias = (TimeZone) testCase[4];
+            OffsetResult expectedMatch = (OffsetResult) testCase[5];
+
+            OffsetResult actualMatch = countryTimeZones.lookupByOffsetWithBias(
+                    totalOffsetMillis, isDst, dstOffsetMillis, whenMillis, bias);
+
+            if (!offsetResultEquals(expectedMatch, actualMatch)) {
+                Function<TimeZone, String> timeZoneFormatter =
+                        x -> x == null ? "null" : x.getID();
+                Function<OffsetResult, String> offsetResultFormatter =
+                        x -> x == null ? "null"
+                                : "{" + x.mTimeZone.getID() + ", " + x.mOneMatch + "}";
+                failures.add("Fail: case=" + i
+                        + ", totalOffsetMillis=" + totalOffsetMillis
+                        + ", isDst=" + isDst
+                        + ", dstOffsetMillis=" + dstOffsetMillis
+                        + ", whenMillis=" + whenMillis
+                        + ", bias=" + timeZoneFormatter.apply(bias)
+                        + ", expectedMatch=" + offsetResultFormatter.apply(expectedMatch)
+                        + ", actualMatch=" + offsetResultFormatter.apply(actualMatch)
+                        + "\n");
+            }
+        }
+        if (!failures.isEmpty()) {
+            fail("Failed:\n" + failures);
+        }
     }
 
     @Test
@@ -737,9 +532,11 @@
         assertEquals(expected, actual);
     }
 
-    private static void assertOffsetResultEquals(OffsetResult expected, OffsetResult actual) {
-        assertEquals(expected.mTimeZone.getID(), actual.mTimeZone.getID());
-        assertEquals(expected.mOneMatch, actual.mOneMatch);
+    private static boolean offsetResultEquals(OffsetResult expected, OffsetResult actual) {
+        return expected == actual
+                || (expected != null && actual != null
+                && Objects.equals(expected.mTimeZone.getID(), actual.mTimeZone.getID())
+                && expected.mOneMatch == actual.mOneMatch);
     }
 
     /**
diff --git a/mmodules/core_platform_api/api/platform/current-api.txt b/mmodules/core_platform_api/api/platform/current-api.txt
index cacc19b..3522502 100644
--- a/mmodules/core_platform_api/api/platform/current-api.txt
+++ b/mmodules/core_platform_api/api/platform/current-api.txt
@@ -1224,7 +1224,7 @@
     method public java.util.List<libcore.timezone.CountryTimeZones.TimeZoneMapping> getTimeZoneMappings();
     method public boolean hasUtcZone(long);
     method public boolean isForCountryCode(String);
-    method @Deprecated public libcore.timezone.CountryTimeZones.OffsetResult lookupByOffsetWithBias(int, boolean, long, android.icu.util.TimeZone);
+    method public libcore.timezone.CountryTimeZones.OffsetResult lookupByOffsetWithBias(int, Boolean, Integer, long, android.icu.util.TimeZone);
   }
 
   public static final class CountryTimeZones.OffsetResult {