5

I want to return a value of a stream based on a condition. Take the following as an example only, where I want to map any apple to Food.APPLE:

public enum Food {
    APPLE, APPLE2, APPLE3, BANANA, PINEAPPLE, CUCUMBER;

    private static final Food[] APPLES = new Food[] {APPLE, APPLE2, APPLE3};

    //java7
    public Food fromValue(String value) {
        for (Food type : Food.values()) {
            if (type.name().equalsIgnoreCase(value)) {
                return ArrayUtils.contains(APPLES, type) ? APPLE : type;
            }
        }
        return null;
    }

    //java8: how to include the array check for APPLES?
    public Food fromValue(String value) {
        return Arrays.stream(Food.values()).
            filter(type -> type.name().equalsIgnoreCase(value))
            .findFirst()
            .orElse(null);
    }
}

How can I include the ternary condition in a stream?

membersound
  • 81,582
  • 193
  • 585
  • 1,120
  • 3
    Well you could use a `map` with the condition inside. – Alexis C. Mar 25 '15 at 12:54
  • @AlexisC. Ok thanks, you probably mean `.map(type -> ArrayUtils.contains(APPLES, type) ? APPLE : type)`? If so, and if that's the best solution, you could add it as an answer. – membersound Mar 25 '15 at 12:56
  • 1
    And what about something like `private static final HashSet APPLES = new HashSet<>(Arrays.asList("apple", "apple2", "apple3"));` and `public static Food fromValue(String value) { return APPLES.contains(value.toLowerCase()) ? APPLE : null; }` ? Constant time in complexity and less code to write. – Alexis C. Mar 25 '15 at 13:03
  • 1
    Ah it's not exactly the same. But your problem could be reduced to this: http://stackoverflow.com/questions/27807232/finding-enum-value-with-java-8-stream-api/27807324#27807324. My suggestion would be to use a map there. – Alexis C. Mar 25 '15 at 13:08

5 Answers5

3

You could do it like this:

import static java.util.AbstractMap.SimpleImmutableEntry;

...

enum Food {
    APPLE, APPLE2, APPLE3, BANANA, PINEAPPLE, CUCUMBER;

    private static final Map<String, Food> MAP = Stream.concat(
                Stream.of(APPLE, APPLE2, APPLE3).map(e -> new SimpleImmutableEntry<>(e.name().toLowerCase(), APPLE)),
                Stream.of(BANANA, PINEAPPLE, CUCUMBER).map(e -> new SimpleImmutableEntry<>(e.name().toLowerCase(), e)))
            .collect(toMap(SimpleImmutableEntry::getKey, SimpleImmutableEntry::getValue));

    public static Food fromValue(String value) {
        return MAP.get(value.toLowerCase());
    }
}

The lookup in the map will be O(1).

Alexis C.
  • 91,686
  • 21
  • 171
  • 177
  • 2
    You don’t need `getOrDefault` to get a null value. You can simply call `get()` as it’s the contract of the ordinary `Map.get` method to return `null` if there is no mapping. Btw. using lowercase might be sufficient for ASCII characters but isn’t a general solution for case insensitive comparison. – Holger Mar 25 '15 at 14:47
2

As Alexis suggested, you can use a map operation

public Food fromValue_v8(String value) {
    return Arrays.stream(Food.values())
        .filter(type-> type.name().equalsIgnoreCase(value))
        .map(type -> ArrayUtils.contains(APPLES, type) ? APPLE : type)
        .findFirst()
        .orElse(null);
}
Ortomala Lokni
  • 56,620
  • 24
  • 188
  • 240
2

There is nothing special to the ternary operator. So you can simply add this mapping operation to the Stream

public Food fromValue(String value) {
    return Arrays.stream(Food.values())
        .filter(type -> type.name().equalsIgnoreCase(value))
        .map(type -> ArrayUtils.contains(APPLES, type)? APPLE: type)
        .findFirst()
        .orElse(null);
}

However, neither of these linear searches is really necessary. Use a Map:

public enum Food {
    APPLE, APPLE2, APPLE3, BANANA, PINEAPPLE, CUCUMBER;

    private static final Map<String,Food> MAP
        = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
    static {
       EnumSet<Food> apples=EnumSet.of(APPLE, APPLE2, APPLE3);
       apples.forEach(apple->MAP.put(apple.name(), APPLE));
       EnumSet.complementOf(apples).forEach(e->MAP.put(e.name(), e));
    }
    public static Food fromValue(String value) {
        return MAP.get(value);
    }
}

It will perform a case insensitive lookup as desired and it is initialized to return the APPLE substitute in the first place so no additional comparison is required.

Holger
  • 285,553
  • 42
  • 434
  • 765
2

According to previous answers, in particular from @Alexis I wrote some code to check booth approach (from Java 7 and Java 8). Maybe this can be useful for new users on Java 8.

So, I've made some changes in original answer. First of all, I put some unit test and I added two wrapping methods verifyNames() and contains(). Second, we can use a default behavior when unexpected action occurs, in this case, when the appleApproachTwo.fromValueJava8() was called with null or an not existing enum value.

Finally, the last change uses the potential uses for java.util.Optional objects. In this case, we can protect the environment to crash due to inconsistency to null objects. There are more discussion about Default Values, Optional and orElse() method at Default Values and Actions

    public enum Food {
    APPLE, APPLE2, APPLE3, BANANA, PINEAPPLE, CUCUMBER, NONE;

    private static final Food[] APPLES = new Food[] {APPLE, APPLE2, APPLE3};

    // approach one
    // java7: conventional use
    public Food fromValueJava7(String value) {
        for (Food type : Food.values()) {
            if (verifyNames(type, value)) {
                return contains(Food.APPLES, type) ? Food.APPLE : type;
            }
        }
        return null;
    }


    // approach two
    // java8: how to include the array check for APPLES?
    public Food fromValueJava8(String value) {
        return Arrays.stream(Food.values())
                .filter(type-> verifyNames(type, value))
                .map(type -> contains(Food.APPLES, type) ? Food.APPLE : type)
                .findFirst()
                .orElse(Food.NONE);
    }

    private boolean contains(Food[] apples, Food type) {
        return ArrayUtils.contains(apples, type);
    }

    private boolean verifyNames(Food type,String other) {
        return type.name().equalsIgnoreCase(other);
    }
    }

    //   FoodTest
    //   
    public class FoodTest {
    @Test
    public void foodTest(){
        Food appleApproachOne  = Food.APPLE;

        // from approach one
        assertEquals( appleApproachOne.fromValueJava7("APPLE"),   Food.APPLE);
        assertEquals( appleApproachOne.fromValueJava7("APPLE2"),  Food.APPLE);
        assertEquals( appleApproachOne.fromValueJava7("APPLE3"),  Food.APPLE);
        assertEquals( appleApproachOne.fromValueJava7("apple3"),  Food.APPLE);
        assertNull  ( appleApproachOne.fromValueJava7("apple4") );
        assertNull  ( appleApproachOne.fromValueJava7(null) );

        Food appleApproachTwo  = Food.APPLE;

        //from approach two
        assertEquals( appleApproachTwo.fromValueJava8("APPLE"),   Food.APPLE);
        assertEquals( appleApproachTwo.fromValueJava8("APPLE2"),  Food.APPLE);
        assertEquals( appleApproachTwo.fromValueJava8("APPLE3"),  Food.APPLE);
        assertEquals( appleApproachTwo.fromValueJava8("apple3"),  Food.APPLE);
        assertEquals( appleApproachOne.fromValueJava8("apple4"),  Food.NONE);
        assertEquals( appleApproachTwo.fromValueJava8(null),      Food.NONE);
    }
}
e2a
  • 952
  • 7
  • 18
0

As others have suggested, using a Map would be better:

import java.util.EnumSet;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class TernaryCondition {

    public enum Food {
        APPLE, APPLE2, APPLE3, BANANA, PINEAPPLE, CUCUMBER;

        private static final EnumSet<Food> APPLES = EnumSet.of(APPLE, APPLE2, APPLE3);

        private static final Map<String, Food> MAP = Stream.of(
            Food.values()).collect(
            Collectors.toMap(
                f -> f.name().toLowerCase(), 
                f -> APPLES.contains(f) ? APPLE : f));

        public static Food fromValue(String value) {
            return MAP.get(value.toLowerCase());
        }
    }

    public static void main(String[] args) {
        Food f = Food.fromValue("apple2");

        System.out.println(f); // APPLE
    }
}

I'd also make fromValue() method static and APPLES an EnumSet. While I realise this answer is very similar to @Holger's, I just wanted to show another approach to build the map.

fps
  • 33,623
  • 8
  • 55
  • 110