diff --git a/.coverage_history b/.coverage_history index f19bf996..6b0bd0e3 100644 --- a/.coverage_history +++ b/.coverage_history @@ -1 +1 @@ -0.9846341092347483 +0.9849253731343284 diff --git a/.github/badges/branches.svg b/.github/badges/branches.svg index b0712222..113fede8 100644 --- a/.github/badges/branches.svg +++ b/.github/badges/branches.svg @@ -1 +1 @@ -branches94.5% \ No newline at end of file +branches95% \ No newline at end of file diff --git a/src/main/java/dev/sorn/fmp4j/exceptions/FmpInvalidPartException.java b/src/main/java/dev/sorn/fmp4j/exceptions/FmpInvalidPartException.java new file mode 100644 index 00000000..cd6001ab --- /dev/null +++ b/src/main/java/dev/sorn/fmp4j/exceptions/FmpInvalidPartException.java @@ -0,0 +1,7 @@ +package dev.sorn.fmp4j.exceptions; + +public class FmpInvalidPartException extends FmpException { + public FmpInvalidPartException(String message, Object... args) { + super(message, args); + } +} diff --git a/src/main/java/dev/sorn/fmp4j/types/FmpPart.java b/src/main/java/dev/sorn/fmp4j/types/FmpPart.java new file mode 100644 index 00000000..4c72f532 --- /dev/null +++ b/src/main/java/dev/sorn/fmp4j/types/FmpPart.java @@ -0,0 +1,77 @@ +package dev.sorn.fmp4j.types; + +import static java.lang.Integer.parseInt; +import static java.lang.String.valueOf; +import static java.util.Objects.compare; + +import dev.sorn.fmp4j.exceptions.FmpInvalidPartException; +import java.io.Serial; + +public final class FmpPart implements Comparable, FmpValueObject { + public static final int MIN_VALUE = 0; + public static final int MAX_VALUE = 1000; + + @Serial + private static final long serialVersionUID = 1L; + + private final int value; + + private FmpPart(int value) { + this.value = value; + } + + public static FmpPart part(String value) { + try { + return part(parseInt(value)); + } catch (NumberFormatException e) { + throw new FmpInvalidPartException("[%s] is not a valid integer value", value); + } + } + + public static FmpPart part(int value) { + if (value < MIN_VALUE) { + throw new FmpInvalidPartException("[%d] is below the minimum allowed value [%d]", value, MIN_VALUE); + } + if (value > MAX_VALUE) { + throw new FmpInvalidPartException("[%d] exceeds the maximum allowed value [%d]", value, MAX_VALUE); + } + return new FmpPart(value); + } + + @Override + public Integer value() { + return value; + } + + @Override + public String toString() { + return valueOf(value); + } + + @Override + public int hashCode() { + return value; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof FmpPart that)) { + return false; + } + return this.value == that.value; + } + + @Override + public int compareTo(FmpPart that) { + if (that == null) { + throw new FmpInvalidPartException("'that.value' is required"); + } + return compare(this.value, that.value, Integer::compareTo); + } +} diff --git a/src/test/java/dev/sorn/fmp4j/types/FmpPartTest.java b/src/test/java/dev/sorn/fmp4j/types/FmpPartTest.java new file mode 100644 index 00000000..f70466d4 --- /dev/null +++ b/src/test/java/dev/sorn/fmp4j/types/FmpPartTest.java @@ -0,0 +1,190 @@ +package dev.sorn.fmp4j.types; + +import static dev.sorn.fmp4j.types.FmpPart.MAX_VALUE; +import static dev.sorn.fmp4j.types.FmpPart.MIN_VALUE; +import static dev.sorn.fmp4j.types.FmpPart.part; +import static java.lang.String.format; +import static java.lang.String.valueOf; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import dev.sorn.fmp4j.exceptions.FmpInvalidPartException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class FmpPartTest { + @ParameterizedTest + @ValueSource(ints = {0, 1000, 954, 241, 741, 2, 15}) + void valid_part(int part) { + // given // when + var p = part(part); + + // then + assertEquals(part, p.value()); + } + + @Test + void below_minimum_part() { + // given + var p = -1; + + // when // then + var e = assertThrows(FmpInvalidPartException.class, () -> part(p)); + assertEquals(format("[%d] is below the minimum allowed value [%d]", p, MIN_VALUE), e.getMessage()); + } + + @Test + void exceeds_maximum_year() { + // given + var p = 1001; + + // when // then + var e = assertThrows(FmpInvalidPartException.class, () -> part(p)); + assertEquals(format("[%d] exceeds the maximum allowed value [%d]", p, MAX_VALUE), e.getMessage()); + } + + @Test + void string_value_not_int() { + // given + var p = "199X"; + + // when // then + var e = assertThrows(FmpInvalidPartException.class, () -> part(p)); + assertEquals(format("[%s] is not a valid integer value", p), e.getMessage()); + } + + @ParameterizedTest + @ValueSource(ints = {1000, 999, 998, 10, 9}) + void toString_returns_value(int part) { + // given // when + var p = part(part); + + // then + assertEquals(p.toString(), valueOf(part)); + } + + @Test + void hashCode_value() { + // given + var part = 999; + var p = part(part); + + // when + var hc = p.hashCode(); + + // then + assertEquals(part, hc); + } + + @Test + void equals_same_true() { + // given + var p = part(1); + + // when + var eq = p.equals(p); + + assertTrue(eq); + } + + @Test + void equals_identical_true() { + // given + var p1 = part(1000); + var p2 = part(1000); + + // when + var eq = p1.equals(p2); + + assertTrue(eq); + } + + @Test + void equals_null_false() { + // given + var p1 = part(1000); + var p2 = (FmpPart) null; + + // when + var eq = p1.equals(p2); + + assertFalse(eq); + } + + @Test + void equals_different_false() { + // given + var p1 = part(1000); + var p2 = part(999); + + // when + var eq = p1.equals(p2); + + assertFalse(eq); + } + + @Test + void equals_wrong_instance_false() { + // given + var p1 = part(999); + var p2 = 999; + + // when + var eq = p1.equals(p2); + + assertFalse(eq); + } + + @Test + void compareTo_null_throws() { + // given + var p1 = part("999"); + var p2 = (FmpPart) null; + + // when // then + var e = assertThrows(FmpInvalidPartException.class, () -> p1.compareTo(p2)); + assertEquals("'that.value' is required", e.getMessage()); + } + + @Test + void compareTo_less_than() { + // given + var p1 = part("1"); + var p2 = part("999"); + + // when + int cmp = p1.compareTo(p2); + + // then + assertEquals(-1, cmp); + } + + @Test + void compareTo_greater_than() { + // given + var p1 = part("999"); + var p2 = part("1"); + + // when + int cmp = p1.compareTo(p2); + + // then + assertEquals(1, cmp); + } + + @Test + void compareTo_equal() { + // given + var p1 = part("1"); + var p2 = part("1"); + + // when + int cmp = p1.compareTo(p2); + + // then + assertEquals(0, cmp); + } +}