Skip to content
/ zeliba Public

A handy DSL-ish library to make comparisons more readable.

License

Notifications You must be signed in to change notification settings

dehasi/zeliba

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

39 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ZeLiba (The library)

A handy DSL-ish helper to make the comparison more readable.

Build Status Maven Central Test Coverage GitHub code size in bytes

Zeliba provides a fluent API to write a comparison (for Comparable<T>) and does other checks.

Inspired by AssertJ, kotlin, ZE FISH

Motivation

Zeliba main points are the following:

  • Provide a fluent API to write a comparison (for Comparable<T>)
  • Make if-checks better align with English grammar
  • Provide pattern matching for Java 8

Fluent compatible

Java doesn't support operator overloading, you can’t write something like a > b for objects, as an alternative you can use Comparable<T>. It makes its job, but it is not very convenient to use.
Look a.compareTo(b) > ??. Every time you need to make small calculations in your head. It’s better (from readability POV) to write a.isGreatherThan(b). Zeliba gives you the ability to do it.
See examples TheComparable, TheChronoLocalDate, TheChonoDateTime

Better English

Usually, util methods start with is prefix (like isEmpty), but negations are covered via exclamation mark !is, which also makes you do calculations. I.e. “if a collection is empty” transforms into collection.isEmpty(), but “if a collection is not empty” transforms into !collection.isEmpty() which is read as “not the collection is empty”. It is obviously grammatically incorrect. Util methods like if(isNotEmpty(collection)) do a great job but still remain grammatically incorrect. We don’t say “if is not an empty collection”. Zeliba provides the same methods but also gives you a fluent API to write grammatically correct code.
See examples. TheObject, TheCollection, TheMap

Pattern matching

Inspired by when from Kotlin.
Since Java 12 case-expressions were extended. But Java 8 is still widely used and it's nice to have some fluent API which is more useful than case for pattern matching. Zeliba provides some pattern-matching features.
See When

Examples

The examples reflect the master branch.

TheComparable

Let's assume we have two comparable objects.

BigDecimal val1 = ...
BigDecimal val2 = ...

Usually we check val1 > val2 like if (val1.compareTo(val2) > 0)

But with TheComparable it's much easier to read

if (the(val1).isGreaterThan(val2)) {
    ...
}

if (the(val2).isLessThan(val1)) {
    ...
}

Fluent interval checks

val1 <= value <= val2

if (the(value).isInTheInterval().fromIncluded(val1).toIncluded(val2)) {
    //...
}

val1 < value < val2

if (the(value).isInTheInterval().fromExcluded(val1).toExcluded(val2)) {
    //...
}

val1 < value <= val2

if (the(value).isInTheInterval().fromExcluded(val1).toIncluded(val2)) {
    //...
}

TheChronoLocalDate

Also, there are extensions to compare dates

LocalDate someDate = ...
LocalDate otherDate = ...

if (the(otherDate).isAfterOrEqual(someDate)) {
 ...
}

if (the(someDate).isNotAfter(otherDate)) {
 ...
}

if (the(otherDate).isBeforeOrEqual(someDate)) {
 ...
}

if (the(someDate).isNotBefore(otherDate)) {
 ...
}

TheChronoLocalDateTime

The same for DateTime

LocalDateTime someDateTime = ...
LocalDateTime otherDateTime = ...

if (the(otherDate).isAfterOrEqual(someDateTime)) {
    ...
}

if (the(someDate).isNotAfter(otherDateTime)) {
    ...
}

if (the(otherDate).isBeforeOrEqual(someDateTime)) {
    ...
}

if (the(someDate).isNotBefore(otherDateTime)) {
    ...
}

TheObject

Fluent null and not equals checks

Object someObject = ...
Object otherObject = ...

if (the(otherObject).isNotEqualTo(someObject)) {
    ...
}

if (the(someObject).isNotNull()) {
    ...
}

if (the(someObject).isNull()) {
    ...
}

TheString

Fluent checks for empty/blank + avoiding NPE

isEmpty

String str1 = null;
String str2 = "abcd";

if (the(str1).isEmpty()) { ... } // returns false
if (the(str2).isNotEmpty()) { ... } // returns true

isBlank

String str1 = null;
String str2 = "abcd";

if (the(str1).isBlank()) { ... } // returns true
if (the(str2).isNotBlank()) { ... } // returns false

substring

Max possible substring

String str = "abcd"

String s = the(str).substring(2, 50); // returns "cd"
String s = the(str).substring(-2, 2); // returns "ab"

replaceAt

Replaces a char at given index

String str = "abcd"

String s = the(str).replaceAt(0, 'x'); // returns "xbcd"
String s = the(str).replaceAt(6, 'x'); // returns "abcd"

TheCollection

Grammatically correct fluent checks if a collection is null or is not empty

List<?> list = ...

if (the(list).isNotEmpty()) { ... }

Set<?> otherSet = null;
if (the(otherSet).isEmpty()) { ... } // returns true

TheMap

Map<?,?>  map = ...
Pair<?,?> pair = ... // Apache Commons Pair<> or any Map.Entry<>

if (the(map).isNotEmpty()) {
    ...
}

contains

Fluent contains checks to check if a map contains the particular entry. Or if the particular key has the particular value.

if (the(map).contains(pair)) {
    ...
}

if (the(map).contains(key, value)) {
    ...
}

if (the(map).contains(entry(key, value))) {
    ...
}

Optional get

Map.get(key) returns null if there is no value. TheMap allows to a map return an Optional<>.

Optional<?> value = the(map).get(key)

When

Pattern-ish matching in pure Java 8

int value = ...

String result = when(value)
    .is(1).then("+")
    .is(0).then("zero")
    .is(-1).then("-")
    .orElse("?"); 

The when returns value from the first matched predicate.

int value = 42

String result = when(value)
    .is(42).then("first_42") //result=first_42
    .is(42).then("second_42")
    .orElse("?"); 

is

The is part accepts Predicate or a value which be compared as Objects.equals

String result = when(value)
    .is(v -> v > 0).then("+")
    .is(0).then("zero") // Objects.equals(0, value)
    .is(v -> v < 0).then("-")
    .orElse("?"); 

isNot

There is an opposise predicate isNot

String result = when(value)
    .isNot(42).then("not 42")
    .orElse("42 for sure"); 

and

To make a conjunction of few is-predicates, and can be used.

int value = 5;

String result = when(value)
    .is(v -> v > 0).and(v-> v < 3).then("(0..3)")
    .is(v -> v > 3).and(v-> v < 7).then("(3..7)")
    .orElse("?");

or

To make a disjunction of few is-predicates, or can be used.

int value = 5;

String result = when(value)
    .is(0).or(2).or(4).then("0 or 2 or 4")
    .is(1).or(3).or(5).then("1 or 3 or 5")
    .orElse("?");

or and and can be used together

int value = 5;

String result = when(value)
    .is(1).or(2).then("< 3")
    .is(v -> v > 6).and(v -> v < 10).or(5).then("(6;10) or 5")
    .is(v -> v > 0).and(v -> v < 5)
        .or(v -> v > 5).and(v -> v < 10).then("(0;5) or (5;10)")
    .orElse("?");

then

then part accepts a value, Supplier or Function. The function accepts the initial value.

int value = ...

String result = when(value)
    .is(1).then("+")
    .is(0).then(() -> "zero")
    .is(v -> v < 0).then(val -> String.valueOf(Math.abs(val))) // string of abs(value)
    .orElse("?"); 

It is also possible to throw an exception from then part

int value = ...

String result = when(value)
    .is(1).then("+")
    .is(0).then(() -> {
        throw new RuntimeException();
    })
    .orElse("?"); 

orElse

orElse accepts the same parameters as then

String result = when(value)
    .is(1).then("1")
    .orElse("not 1");
String result = when(value)
    .is(1).then("1")
    .orElse(this::method); // method will be called only if value is not 1
String result = when(value)
    .is(1).then("1")
    .orElse(val -> String.valueOf(Math.abs(val))); 

orElseThrow

By default orElseThrow throws IllegalStateException with default message. orElseThrow accepts a String to set an exception message, or Supplier to throw a custom one.

String result = when(value)
    .is(1).then("1")
    .orElseThrow(); // IllegalStateException with default message
String result = when(value)
    .is(1).then("1")
    .orElseThrow("Some valuable message");
String result = when(value)
    .is(1).then("1")
    .orElseThrow(RuntimeException::new); 

asOptional

If the absence of the result is normal flow. Optional<> can be used as a return value.

int value = 1;

Optional<String> result = when(value)
    .is(0).then("0")
    .is(1).then("1")
    .asOptional(); // Optional.of("1")

Optional<String> result = when(value)
    .is(0).then("0")
    .is(2).then("2")
    .asOptional(); // Optional.empty()

Complex example

String result = when(value)
    .is(i -> i < 0).then(i -> String.format("negative %s", -i))
    .is(0).then("zero")
    .is(1).then(() -> String.format("positive %s", value))
    .is(100_500).then(() -> {
            throw new RuntimeException();
    })
    .isNot(42).then("not 42")
    .orElseThrow("Custom exception message");

When2

It's possible to make matching with two variables

int x = 1;
int y = -2;

String result = when(x, y)
    .is(0, 0).then("zero")
    .is(p -> p > 0, p -> p > 0).then("I Quadrant")
    .is(p -> p < 0, p -> p > 0).then("II Quadrant")
    .is(p -> p < 0, p -> p < 0).then("III Quadrant")
    .is(p -> p > 0, p -> p < 0).then("IV Quadrant")
    .orElse("??");

and is also supported

int x = 1, y = 1;
String result = when(x, y)
    .is((v1, v2) -> v1 + v2 < 0).and((v1, v2) -> v1 + v2 > -10).then("x+y=(-10..0)")
    .is((v1, v2) -> v1 + v2 > -0).and((v1, v2) -> v1 + v2 < 10).then("x+y=(0..10)")
    .is((v1, v2) -> v1 + v2 > 10).and((v1, v2) -> v1 + v2 < 20).then("x+y=(10..20)")
    .orElseThrow();

or and and can be used together

String result = when(x, y)
    .is(2, 2).or(3, 3).or(4, 4).then("2-3-4")
    .isNot(1, 1).and(p -> p > 0, p -> p > 0).then("not 1, > 0")
    .is(1, 2).or(2, 1).or(1, 1).then("1 or 2")
    .orElseThrow();

License

This project is licensed under Apache License, version 2.0

Installation

Releases are available in Maven Central

Maven

Add this snippet to the pom.xml dependencies section:

<dependency>
    <groupId>me.dehasi</groupId>
    <artifactId>zeliba</artifactId>
    <version>2021.06.22</version>
</dependency>

Gradle

Add this snippet to the build.gradle dependencies section:

implementation 'me.dehasi:zeliba:2021.06.22'

Contribution

Feel free to share your ideas via issues and pull-requests.