code: private Range<String> transformClosed(Range<String> range) {
      if (range.hasLowerBound()) {
        String lower = range.lowerEndpoint();
        // the special case, (a, _] and apply(a) == a is handled by skipping a
        String afterLower = domain.next(apply(lower));
        if (afterLower != null) {
          if (range.hasUpperBound()) {
            String upper = range.upperEndpoint();
            String upperImage = apply(upper);
            // meaning: at the endpoint
            if (upper.equals(upperImage) && range.isUpperBoundClosed()) {
              // include upper
              return Ranges.closed(afterLower, upperImage);
            } else {
              String beforeUpper = domain.previous(upperImage);
              if (afterLower.compareTo(beforeUpper) <= 0) {
                return Ranges.closed(afterLower, beforeUpper);
              }
            }
          } else {
            return Ranges.atLeast(afterLower);
          }
        }
      } else if (range.hasUpperBound()) {
        String upper = range.upperEndpoint();
        String upperImage = apply(upper);
        if (upper.equals(upperImage) && range.isUpperBoundClosed()) {
          // include upper
          return Ranges.atMost(upperImage);
        } else {
          String beforeUpper = domain.previous(upperImage);
          if (beforeUpper != null) {
            return Ranges.atMost(beforeUpper);
          }
        }
      }
      return null;
    }
docs: Transforms a Range predicate to a closed range on this partitioner's upper
  bounds. Handles edge cases correctly.
  
  @param range a Range of Strings
  @return a Range of upper-bound Strings