| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.sis.measure; |
| |
| import java.util.Date; |
| import java.util.Locale; |
| import java.util.TimeZone; |
| import java.time.Instant; |
| import java.time.LocalTime; |
| import java.time.LocalDate; |
| import java.text.DateFormat; |
| import java.text.FieldPosition; |
| import java.text.ParsePosition; |
| import java.text.ParseException; |
| import java.text.AttributedCharacterIterator; |
| import static java.lang.StrictMath.*; |
| import static java.lang.Double.POSITIVE_INFINITY; |
| import static java.lang.Double.NEGATIVE_INFINITY; |
| import static org.apache.sis.util.privy.Constants.UTC; |
| |
| // Test dependencies |
| import org.junit.jupiter.api.Test; |
| import static org.junit.jupiter.api.Assertions.*; |
| import org.apache.sis.test.TestCase; |
| |
| |
| /** |
| * Tests parsing and formatting done by the {@link RangeFormat} class. |
| * |
| * @author Martin Desruisseaux (Geomatys) |
| */ |
| public final class RangeFormatTest extends TestCase { |
| /** |
| * The format being tested. |
| */ |
| private RangeFormat format; |
| |
| /** |
| * The position of the minimal value and maximal value fields. |
| */ |
| private FieldPosition minPos, maxPos; |
| |
| /** |
| * The position during parsing. |
| */ |
| private ParsePosition parsePos; |
| |
| /** |
| * Creates a new test case. |
| */ |
| public RangeFormatTest() { |
| } |
| |
| /** |
| * Formats the given range. |
| */ |
| private String format(final Range<?> range) { |
| final String s1 = format.format(range, new StringBuffer(), minPos).toString(); |
| final String s2 = format.format(range, new StringBuffer(), maxPos).toString(); |
| assertEquals(s1, s2, "Two consecutive formats produced different results."); |
| return s1; |
| } |
| |
| /** |
| * Parses the given text and ensure that there is only whitespace or underscore after the last |
| * parse position. Also verifies that there is no underscore before the parse position. This |
| * assume that the underscore is not allowed to appears in the valid portion of the text. |
| */ |
| private Range<?> parse(final String text) { |
| parsePos.setIndex(0); |
| final Range<?> range = format.parse(text, parsePos); |
| assertEquals(parsePos.getIndex() == 0, range == null, "Index position shall be modified on parse success, and only parse success."); |
| assertEquals(parsePos.getErrorIndex() >= 0, range == null, "Error position shall be defined on parse failure, and only parse failure"); |
| if (range != null) { |
| int i = parsePos.getIndex(); |
| assertTrue(text.lastIndexOf('_', i-1) < 0, "The parse method parsed a character that it shouldn't have accepted."); |
| while (i < text.length()) { |
| final char c = text.charAt(i++); |
| assertTrue(Character.isWhitespace(c) || c == '_', "Looks like that the parse method didn't parsed everything."); |
| } |
| } |
| return range; |
| } |
| |
| /** |
| * Tests the {@link RangeFormat#format(Object, StringBuffer, FieldPosition)} method with numbers. |
| */ |
| @Test |
| public void testFormatNumbers() { |
| format = new RangeFormat(Locale.CANADA); |
| minPos = new FieldPosition(RangeFormat.Field.MIN_VALUE); |
| maxPos = new FieldPosition(RangeFormat.Field.MAX_VALUE); |
| |
| // Closed range |
| assertEquals("[-10 … 20]", format(NumberRange.create(-10, true, 20, true))); |
| assertEquals(1, minPos.getBeginIndex()); |
| assertEquals(4, minPos.getEndIndex()); |
| assertEquals(7, maxPos.getBeginIndex()); |
| assertEquals(9, maxPos.getEndIndex()); |
| |
| // Open range |
| assertEquals("(-3 … 4)", format(NumberRange.create(-3, false, 4, false))); |
| assertEquals(1, minPos.getBeginIndex()); |
| assertEquals(3, minPos.getEndIndex()); |
| assertEquals(6, maxPos.getBeginIndex()); |
| assertEquals(7, maxPos.getEndIndex()); |
| |
| // Half-open range |
| assertEquals("[2 … 8)", format(NumberRange.create(2, true, 8, false))); |
| assertEquals(1, minPos.getBeginIndex()); |
| assertEquals(2, minPos.getEndIndex()); |
| assertEquals(5, maxPos.getBeginIndex()); |
| assertEquals(6, maxPos.getEndIndex()); |
| |
| // Half-open range |
| assertEquals("(40 … 90]", format(NumberRange.create(40, false, 90, true))); |
| assertEquals(1, minPos.getBeginIndex()); |
| assertEquals(3, minPos.getEndIndex()); |
| assertEquals(6, maxPos.getBeginIndex()); |
| assertEquals(8, maxPos.getEndIndex()); |
| |
| // Single value |
| assertEquals("{300}", format(NumberRange.create(300, true, 300, true))); |
| assertEquals(1, minPos.getBeginIndex()); |
| assertEquals(4, minPos.getEndIndex()); |
| assertEquals(1, maxPos.getBeginIndex()); |
| assertEquals(4, maxPos.getEndIndex()); |
| |
| // Empty range |
| assertEquals("{}", format(NumberRange.create(300, true, 300, false))); |
| assertEquals(1, minPos.getBeginIndex()); |
| assertEquals(1, minPos.getEndIndex()); |
| assertEquals(1, maxPos.getBeginIndex()); |
| assertEquals(1, maxPos.getEndIndex()); |
| |
| // Negative infinity |
| assertEquals("(−∞ … 30]", format(NumberRange.create(Double.NEGATIVE_INFINITY, true, 30, true))); |
| assertEquals(1, minPos.getBeginIndex()); |
| assertEquals(3, minPos.getEndIndex()); |
| assertEquals(6, maxPos.getBeginIndex()); |
| assertEquals(8, maxPos.getEndIndex()); |
| |
| // Positive infinity |
| assertEquals("[50 … ∞)", format(NumberRange.create(50, true, Double.POSITIVE_INFINITY, true))); |
| assertEquals(1, minPos.getBeginIndex()); |
| assertEquals(3, minPos.getEndIndex()); |
| assertEquals(6, maxPos.getBeginIndex()); |
| assertEquals(7, maxPos.getEndIndex()); |
| |
| // Positive infinities |
| assertEquals("(−∞ … ∞)", format(NumberRange.create(Double.NEGATIVE_INFINITY, true, Double.POSITIVE_INFINITY, true))); |
| assertEquals(1, minPos.getBeginIndex()); |
| assertEquals(3, minPos.getEndIndex()); |
| assertEquals(6, maxPos.getBeginIndex()); |
| assertEquals(7, maxPos.getEndIndex()); |
| |
| // Positive infinity with integers |
| assertEquals("[50 … ∞)", format(new NumberRange<>(Integer.class, 50, true, null, true))); |
| assertEquals(1, minPos.getBeginIndex()); |
| assertEquals(3, minPos.getEndIndex()); |
| assertEquals(6, maxPos.getBeginIndex()); |
| assertEquals(7, maxPos.getEndIndex()); |
| |
| // Negative infinity with integers |
| assertEquals("(−∞ … 40]", format(new NumberRange<>(Integer.class, null, true, 40, true))); |
| assertEquals(1, minPos.getBeginIndex()); |
| assertEquals(3, minPos.getEndIndex()); |
| assertEquals(6, maxPos.getBeginIndex()); |
| assertEquals(8, maxPos.getEndIndex()); |
| |
| // Measurement |
| assertEquals("[-10 … 20] m", format(MeasurementRange.create(-10, true, 20, true, Units.METRE))); |
| assertEquals(1, minPos.getBeginIndex()); |
| assertEquals(4, minPos.getEndIndex()); |
| assertEquals(7, maxPos.getBeginIndex()); |
| assertEquals(9, maxPos.getEndIndex()); |
| |
| assertEquals("[-10 … 20]°", format(MeasurementRange.create(-10, true, 20, true, Units.DEGREE))); |
| assertEquals(1, minPos.getBeginIndex()); |
| assertEquals(4, minPos.getEndIndex()); |
| assertEquals(7, maxPos.getBeginIndex()); |
| assertEquals(9, maxPos.getEndIndex()); |
| |
| maxPos = new FieldPosition(RangeFormat.Field.UNIT); |
| assertEquals("[-1 … 2] km", format(MeasurementRange.create(-1, true, 2, true, Units.KILOMETRE))); |
| assertEquals( 9, maxPos.getBeginIndex()); |
| assertEquals(11, maxPos.getEndIndex()); |
| } |
| |
| /** |
| * Tests the {@link RangeFormat#format(Object, StringBuffer, FieldPosition)} method |
| * using the alternate format. |
| */ |
| @Test |
| public void testAlternateFormat() { |
| format = new RangeFormat(Locale.CANADA); |
| minPos = new FieldPosition(RangeFormat.Field.MIN_VALUE); |
| maxPos = new FieldPosition(RangeFormat.Field.MAX_VALUE); |
| format.setAlternateForm(true); |
| |
| assertEquals("[-10 … 20]", format(NumberRange.create(-10, true, 20, true))); |
| assertEquals("]-3 … 4[", format(NumberRange.create( -3, false, 4, false))); |
| assertEquals("[2 … 8[", format(NumberRange.create( 2, true, 8, false))); |
| } |
| |
| /** |
| * Tests the parsing method on ranges of numbers. This test fixes the type to |
| * {@code Integer.class}. A different test will let the parser determine the |
| * type itself. |
| */ |
| @Test |
| public void testParseIntegers() { |
| format = new RangeFormat(Locale.CANADA, Integer.class); |
| parsePos = new ParsePosition(0); |
| |
| assertEquals(NumberRange.create(-10, true, 20, true ), parse("[-10 … 20]" )); |
| assertEquals(NumberRange.create( -3, false, 4, false), parse("( -3 … 4) ")); |
| assertEquals(NumberRange.create( 2, true, 8, false), parse(" [2 … 8) ")); |
| assertEquals(NumberRange.create( 40, false, 90, true ), parse(" (40 … 90]")); |
| assertEquals(NumberRange.create(300, true, 300, true ), parse(" 300 ")); |
| assertEquals(NumberRange.create(300, true, 300, true ), parse("[300]")); |
| assertEquals(NumberRange.create(300, false, 300, false), parse("(300)")); |
| assertEquals(NumberRange.create(300, true, 300, true ), parse("{300}")); |
| assertEquals(NumberRange.create( 0, true, 0, false), parse("[]")); |
| assertEquals(NumberRange.create( 0, true, 0, false), parse("{}")); |
| } |
| |
| /** |
| * Tests the parsing method on ranges of numbers. This test fixes the type to |
| * {@code Double.class}. A different test will let the parser determine the |
| * type itself. |
| */ |
| @Test |
| public void testParseDoubles() { |
| format = new RangeFormat(Locale.CANADA, Double.class); |
| parsePos = new ParsePosition(0); |
| |
| assertEquals(NumberRange.create(-10.0, true, 20.0, true), parse("[-10 … 20]" )); |
| assertEquals(NumberRange.create(NEGATIVE_INFINITY, true, 30.0, true), parse("[-∞ … 30]")); |
| assertEquals(NumberRange.create(NEGATIVE_INFINITY, true, 30.0, true), parse("[−∞ … 30]")); |
| assertEquals(NumberRange.create(50.0, true, POSITIVE_INFINITY, true), parse("[50 … ∞]")); |
| } |
| |
| /** |
| * Tests the parsing method on ranges of numbers where the type is inferred automatically. |
| */ |
| @Test |
| @SuppressWarnings("cast") |
| public void testParseAuto() { |
| format = new RangeFormat(Locale.CANADA); |
| parsePos = new ParsePosition(0); |
| |
| assertEquals(NumberRange.create((byte) -10, true, (byte) 20, true), parse("[ -10 … 20]" )); |
| assertEquals(NumberRange.create((short) -1000, true, (short) 2000, true), parse("[-1000 … 2000]" )); |
| assertEquals(NumberRange.create((int) 10, true, (int) 40000, true), parse("[ 10 … 40000]" )); |
| assertEquals(NumberRange.create((int) 1, true, (int) 50000, true), parse("[ 1.00 … 50000]" )); |
| assertEquals(NumberRange.create((float) 8.5, true, (float) 4, true), parse("[ 8.50 … 4]" )); |
| } |
| |
| /** |
| * Tests the parsing of invalid ranges. |
| */ |
| @Test |
| public void testParseFailure() { |
| format = new RangeFormat(Locale.CANADA); |
| parsePos = new ParsePosition(0); |
| |
| assertNull(parse("[-A … 20]")); assertEquals(1, parsePos.getErrorIndex()); |
| assertNull(parse("[10 … TB]")); assertEquals(6, parsePos.getErrorIndex()); |
| assertNull(parse("[10 x 20]")); assertEquals(4, parsePos.getErrorIndex()); |
| assertNull(parse("[10 … 20" )); assertEquals(8, parsePos.getErrorIndex()); |
| var e = assertThrows(ParseException.class, () -> format.parse("[10 … TB]")); |
| assertEquals(6, e.getErrorOffset()); |
| } |
| |
| /** |
| * Stores the field indices of the years in the {@link #minPos} and {@link #maxPos} fields. |
| */ |
| private static void findYears(final AttributedCharacterIterator it, |
| final RangeFormat.Field field, final FieldPosition pos) |
| { |
| it.setIndex(it.getRunLimit(field)); |
| it.setIndex(it.getRunLimit(DateFormat.Field.YEAR)); |
| pos.setBeginIndex(it.getIndex()); |
| it.setIndex(it.getRunLimit(DateFormat.Field.YEAR)); |
| pos.setEndIndex(it.getIndex()); |
| } |
| |
| /** |
| * Tests formatting a range of {@link LocalDate} objects. |
| */ |
| @Test |
| public void testFormatLocalDate() { |
| format = new RangeFormat(Locale.CANADA_FRENCH, LocalDate.class); |
| final Range<LocalDate> range = new Range<>(LocalDate.class, |
| LocalDate.parse("2019-12-23"), true, |
| LocalDate.parse("2020-05-31"), true); |
| /* |
| * Expected output is "[2019-12-23 … 2020-05-31]" but be robust |
| * to small variation in output format (years may be on 2 digits). |
| */ |
| final String result = format.format(range); |
| assertTrue(result.matches("\\[(20)?19-12-23 … (20)?20-05-31\\]"), result); |
| } |
| |
| /** |
| * Tests formatting a range of {@link LocalTime} objects. |
| */ |
| @Test |
| public void testFormatLocalTime() { |
| format = new RangeFormat(Locale.FRANCE, LocalTime.class); |
| final Range<LocalTime> range = new Range<>(LocalTime.class, |
| LocalTime.parse("06:00:00"), true, |
| LocalTime.parse("18:00:00"), true); |
| |
| assertEquals("[06:00 … 18:00]", format.format(range)); |
| } |
| |
| /** |
| * Tests formatting a range of {@link Instant} objects. |
| * |
| * @see RangeTest#testFormatInstantTo() |
| */ |
| @Test |
| public void testFormatInstant() { |
| format = new RangeFormat(Locale.FRANCE, Instant.class); |
| ((DateFormat) format.elementFormat).setTimeZone(TimeZone.getTimeZone("Europe/Paris")); |
| final Range<Instant> range = new Range<>(Instant.class, |
| Instant.parse("2019-12-23T06:00:00Z"), true, |
| Instant.parse("2020-05-31T18:00:00Z"), true); |
| |
| assertEquals("[23/12/2019 07:00 … 31/05/2020 20:00]", format.format(range)); |
| } |
| |
| /** |
| * Tests the {@link RangeFormat#formatToCharacterIterator(Object)} method with dates. |
| */ |
| @Test |
| public void testFormatDatesToCharacterIterator() { |
| format = new RangeFormat(Locale.FRANCE, TimeZone.getTimeZone(UTC)); |
| minPos = new FieldPosition(RangeFormat.Field.MIN_VALUE); |
| maxPos = new FieldPosition(RangeFormat.Field.MAX_VALUE); |
| parsePos = new ParsePosition(0); |
| |
| final long HOUR = 60L * 60 * 1000; |
| final long DAY = 24L * HOUR; |
| final long YEAR = round(365.25 * DAY); |
| |
| Range<Date> range = new Range<>(Date.class, |
| new Date(15*DAY + 18*HOUR), true, |
| new Date(20*YEAR + 15*DAY + 9*HOUR), true); |
| AttributedCharacterIterator it = format.formatToCharacterIterator(range); |
| String text = it.toString(); |
| findYears(it, RangeFormat.Field.MIN_VALUE, minPos); |
| findYears(it, RangeFormat.Field.MAX_VALUE, maxPos); |
| assertEquals("[16/01/1970 18:00 … 16/01/1990 09:00]", text); |
| assertEquals( 7, minPos.getBeginIndex()); |
| assertEquals(11, minPos.getEndIndex()); |
| assertEquals(26, maxPos.getBeginIndex()); |
| assertEquals(30, maxPos.getEndIndex()); |
| assertEquals(range, parse(text)); |
| /* |
| * Try again with the infinity symbol in one endpoint. |
| */ |
| range = new Range<>(Date.class, (Date) null, true, new Date(20*YEAR), true); |
| it = format.formatToCharacterIterator(range); |
| text = it.toString(); |
| findYears(it, RangeFormat.Field.MAX_VALUE, maxPos); |
| assertEquals("(−∞ … 01/01/1990 00:00]", text); |
| assertEquals(12, maxPos.getBeginIndex()); |
| assertEquals(16, maxPos.getEndIndex()); |
| assertEquals(range, parse(text)); |
| |
| range = new Range<>(Date.class, new Date(20*YEAR), true, (Date) null, true); |
| it = format.formatToCharacterIterator(range); |
| text = it.toString(); |
| findYears(it, RangeFormat.Field.MIN_VALUE, minPos); |
| assertEquals("[01/01/1990 00:00 … ∞)", text); |
| assertEquals( 7, minPos.getBeginIndex()); |
| assertEquals(11, minPos.getEndIndex()); |
| assertEquals(range, parse(text)); |
| } |
| |
| /** |
| * Tests {@link RangeFormat#clone()}. |
| */ |
| @Test |
| public void testClone() { |
| final RangeFormat f1 = new RangeFormat(Locale.FRANCE); |
| f1.setElementPattern("#0.###", false); |
| final RangeFormat f2 = f1.clone(); |
| f2.setElementPattern("#0.00#", false); |
| assertEquals("#0.###", f1.getElementPattern(false)); |
| assertEquals("#0.00#", f2.getElementPattern(false)); |
| } |
| } |