I'm glad to solving your problem since it is a good example for introducing the Composite Design Pattern in Functional Programming. you can composing functions into a bigger and  powerful single function. for example:
Function<String, Optional<Range<LocalDateTime>>> parser = anyOf(
        both(), //case 1
        starting(), //case 2
        ending(), //case 3
        since(LocalDateTime.now()) //case 4
);
Range<LocalDateTime> range = parser.apply("<INPUT>").orElse(null);
//OR using in stream as below
List<Range<LocalDateTime>> result = Stream.of(
    "<Case 1>", "<Case 2>", "<Case 3>", "<Case 4>"
).map(parser).filter(Optional::isPresent).map(Optional::get).collect(toList());
Let's introduce the code above each step by step
the code below almost applies the most of Design Patterns in OOP. e.g: Composite, Proxy, Adapter, Factory Method Design Patterns and .etc.
Functions
factory: the both method meet the 1st case as below:
static Function<String, Optional<Range<LocalDateTime>>> both() {
    return parsing((first, second) -> new Range<>(
            datetime(first),
            datetime(second)
    ));
}
factory: the starting method meet the 2nd case as below:
static Function<String, Optional<Range<LocalDateTime>>> starting() {
        return parsing((first, second) -> {
            LocalDateTime start = datetime(first);
            return new Range<>(start, start.plus(amount(second)));
        });
    }
factory: the ending method meet the 3rd case as below:
static Function<String, Optional<Range<LocalDateTime>>> ending() {
    return parsing((first, second) -> {
        LocalDateTime end = datetime(second);
        return new Range<>(end.minus(amount(first)), end);
    });
}
factory: the since method meet the last case as below:
static Function<String,Optional<Range<LocalDateTime>>> since(LocalDateTime start) {
    return parsing((amount, __) -> new Range<>(start, start.plus(amount(amount))));
}
composite : the responsibility of the anyOf method is find the satisfied result among the Functions as quickly as possible:
@SuppressWarnings("ConstantConditions")
static <T, R> Function<T, Optional<R>>
anyOf(Function<T, Optional<R>>... functions) {
    return it -> Stream.of(functions).map(current -> current.apply(it))
            .filter(Optional::isPresent)
            .findFirst().get();
}
adapter: the responsibility of the parsing method is create a parser for a certain input:
static <R> Function<String, Optional<R>> 
parsing(BiFunction<String, String, R> parser) {
    return splitting("/", exceptionally(optional(parser), Optional::empty));
}
proxy: the responsibility of the exceptionally method is handling Exceptions:
static <T, U, R> BiFunction<T, U, R>
exceptionally(BiFunction<T, U, R> source, Supplier<R> exceptional) {
    return (first, second) -> {
        try {
            return source.apply(first, second);
        } catch (Exception ex) {
            return exceptional.get();
        }
    };
}
adapter: the responsibility of the splitting method is translates a BiFunction to a Function:
static <R> Function<String, R>
splitting(String regex, BiFunction<String, String, R> source) {
    return value -> {
        String[] parts = value.split(regex);
        return source.apply(parts[0], parts.length == 1 ? "" : parts[1]);
    };
}
adapter: the responsibility of the optional method is create an Optional for the final result:
static <R> BiFunction<String, String, Optional<R>> 
optional(BiFunction<String, String, R> source) {
    return (first, last) -> Optional.of(source.apply(first, last));
}
the Range class for saving a ranged thing:
final class Range<T> {
    public final T start;
    public final T end;
    public Range(T start, T end) {
        this.start = start;
        this.end = end;
    }
    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Range)) {
            return false;
        }
        Range<?> that = (Range<?>) o;
        return Objects.equals(start, that.start) && Objects.equals(end, that.end);
    }
    @Override
    public int hashCode() {
        return Objects.hash(start) * 31 + Objects.hash(end);
    }
    @Override
    public String toString() {
        return String.format("[%s, %s]", start, end);
    }
}
Utilities
the datetime method creates a LocalDateTime from a String:
static LocalDateTime datetime(String datetime) {
    return LocalDateTime.parse(
            datetime, 
            DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss['Z']")
    );
}    
the amount method creates a TemporalAmount that takes both a Duration and a Period from a String: 
static TemporalAmount amount(String text) {
    return splitting("T", (first, second) -> new TemporalAmount() {
        private Period period= first.isEmpty() ? Period.ZERO : Period.parse(first);
        private Duration duration = second.isEmpty() ? Duration.ZERO
                : Duration.parse(String.format("PT%s", second));
        @Override
        public long get(TemporalUnit unit) {
            return (period.getUnits().contains(unit) ? period.get(unit) : 0) +
                   (duration.getUnits().contains(unit) ? duration.get(unit) : 0);
        }
        @Override
        public List<TemporalUnit> getUnits() {
            return Stream.of(period, duration).map(TemporalAmount::getUnits)
                                              .flatMap(List::stream)
                                              .collect(toList());
        }
        @Override
        public Temporal addTo(Temporal temporal) {
            return period.addTo(duration.addTo(temporal));
        }
        @Override
        public Temporal subtractFrom(Temporal temporal) {
            return period.subtractFrom(duration.subtractFrom(temporal));
        }
    }).apply(text);
}