Vipin
Vipin

Reputation: 5223

Find number of elements in range from map object

Map structure and data is given below

Map<String, BigDecimal>

Now i want to group values in range, output has range as key and number of elements there as value. Like below:

How can we do this using java streams ?

Upvotes: 5

Views: 1901

Answers (8)

marsouf
marsouf

Reputation: 1147

Assuming your range has the value BigDecimal.valueOf(26), you can do the following to get a Map<BigDecimal, Long> where each key represents the group id (0 for [0-25], 1 for [26, 51], ...), and each corresponding value represents the group count of elements.

content.values()
    .stream()
    .collect(Collectors.groupingBy(n -> n.divide(range, BigDecimal.ROUND_FLOOR), Collectors.counting()))

Upvotes: 0

Holger
Holger

Reputation: 298143

You may use a solution for regular ranges, e.g.

BigDecimal range = BigDecimal.valueOf(25);
inputMap.values().stream()
        .collect(Collectors.groupingBy(
            bd -> bd.subtract(BigDecimal.ONE).divide(range, 0, RoundingMode.DOWN),
            TreeMap::new, Collectors.counting()))
        .forEach((group,count) -> {
            group = group.multiply(range);
            System.out.printf("%3.0f - %3.0f: %s%n",
                              group.add(BigDecimal.ONE), group.add(range), count);
        });

which will print:

  1 -  25: 2
 51 -  75: 1
 76 - 100: 1

(not using the irregular range 0 - 25)

or a solution with explicit ranges:

TreeMap<BigDecimal,String> ranges = new TreeMap<>();
ranges.put(BigDecimal.ZERO,        " 0 - 25");
ranges.put(BigDecimal.valueOf(26), "26 - 50");
ranges.put(BigDecimal.valueOf(51), "51 - 75");
ranges.put(BigDecimal.valueOf(76), "76 - 99");
ranges.put(BigDecimal.valueOf(100),">= 100 ");

inputMap.values().stream()
        .collect(Collectors.groupingBy(
            bd -> ranges.floorEntry(bd).getValue(), TreeMap::new, Collectors.counting()))
        .forEach((group,count) -> System.out.printf("%s: %s%n", group, count));
 0 - 25: 2
51 - 75: 1
76 - 99: 1

which can also get extended to print the absent ranges:

Map<BigDecimal, Long> groupToCount = inputMap.values().stream()
    .collect(Collectors.groupingBy(bd -> ranges.floorKey(bd), Collectors.counting()));
ranges.forEach((k, g) -> System.out.println(g+": "+groupToCount.getOrDefault(k, 0L)));
 0 - 25: 2
26 - 50: 0
51 - 75: 1
76 - 99: 1
>= 100 : 0

But note that putting numeric values into ranges like, e.g. “0 - 25” and “26 - 50” only makes sense if we’re talking about whole numbers, precluding values between 25 and 26, raising the question why you’re using BigDecimal instead of BigInteger. For decimal numbers, you would normally use ranges like “0 (inclusive) - 25 (exclusive)” and “25 (inclusive) - 50 (exclusive)”, etc.

Upvotes: 5

Eugene
Eugene

Reputation: 120848

If only RangeMap from guava had methods like replace of computeIfPresent/computeIfAbsent like the additions in java-8 Map do, this would have been a breeze to do. Otherwise it's a bit cumbersome:

Map<String, BigDecimal> left = new HashMap<>();

left.put("A", new BigDecimal(12));
left.put("B", new BigDecimal(23));
left.put("C", new BigDecimal(67));
left.put("D", new BigDecimal(99));



    RangeMap<BigDecimal, Long> ranges = TreeRangeMap.create();
    ranges.put(Range.closedOpen(new BigDecimal(0), new BigDecimal(25)), 0L);
    ranges.put(Range.closedOpen(new BigDecimal(25), new BigDecimal(50)), 0L);
    ranges.put(Range.closedOpen(new BigDecimal(50), new BigDecimal(75)), 0L);
    ranges.put(Range.closedOpen(new BigDecimal(75), new BigDecimal(100)), 0L);

    left.values()
            .stream()
            .forEachOrdered(x -> {
                Entry<Range<BigDecimal>, Long> e = ranges.getEntry(x);
                ranges.put(e.getKey(), e.getValue() + 1);
            });

    System.out.println(ranges);

Upvotes: 1

Mincong Huang
Mincong Huang

Reputation: 5552

You can also use a NavigableMap:

Map<String, BigDecimal> dataSet = new HashMap<>();
dataSet.put("A", new BigDecimal(12));
dataSet.put("B", new BigDecimal(23));
dataSet.put("C", new BigDecimal(67));
dataSet.put("D", new BigDecimal(99));

// Map(k=MinValue, v=Count)
NavigableMap<BigDecimal, Integer> partitions = new TreeMap<>();
partitions.put(new BigDecimal(0), 0);
partitions.put(new BigDecimal(25), 0);
partitions.put(new BigDecimal(50), 0);
partitions.put(new BigDecimal(75), 0);
partitions.put(new BigDecimal(100), 0);

for (BigDecimal d : dataSet.values()) {
  Entry<BigDecimal, Integer> e = partitions.floorEntry(d);
  partitions.put(e.getKey(), e.getValue() + 1);
}

partitions.forEach((k, count) -> System.out.println(k + ": " + count));
// 0: 2
// 25: 0
// 50: 1
// 75: 1
// 100: 0

Upvotes: 1

Master Chief
Master Chief

Reputation: 2541

This will give you a similar result.

public static void main(String[] args) {
    Map<String, Integer> resMap = new HashMap<>();
    int range = 25;

    Map<String, BigDecimal> aMap=new HashMap<>();

    aMap.put("A",new BigDecimal(12));
    aMap.put("B",new BigDecimal(23));
    aMap.put("C",new BigDecimal(67));
    aMap.put("D",new BigDecimal(99));

    aMap.values().forEach(v -> {
        int lower = v.divide(new BigDecimal(range)).intValue();
        // get the lower & add the range to get higher
        String key = lower*range + "-" + (lower*range+range-1);
        resMap.put(key, resMap.getOrDefault(key, 0) + 1);
    });

    resMap.entrySet().forEach(e -> System.out.println(e.getKey() + " = " + e.getValue()));
}

Though there are some differences from what you have asked

  • Ranges are inclusive in this; 0-24 instead of 0-25, so that 25 is included in 25-50
  • Your range 0-25 contains 26 possible values in between, while all other ranges contain 25 values. This implementations output has ranges of size 25 (configurable via range variable)
  • You can decide on the range

Output (you may want to iterate the map's key better to get the output in a sorted order)

75-99 = 1
0-24 = 2
50-74 = 1

Upvotes: 0

Jorn Vernee
Jorn Vernee

Reputation: 33865

If you have a Range like this:

class Range {
    private final BigDecimal start;
    private final BigDecimal end;

    public Range(BigDecimal start, BigDecimal end) {
        this.start = start;
        this.end = end;
    }

    public boolean inRange(BigDecimal val) {
        return val.compareTo(start) >= 0 && val.compareTo(end) <= 0;
    }

    @Override
    public String toString() {
        return start + "-" + end;
    }

}

You can do this:

Map<String, BigDecimal> input = new HashMap<>();
input.put("A", BigDecimal.valueOf(12));
input.put("B", BigDecimal.valueOf(23));
input.put("C", BigDecimal.valueOf(67));
input.put("D", BigDecimal.valueOf(99));

List<Range> ranges = new ArrayList<>();
ranges.add(new Range(BigDecimal.valueOf(0), BigDecimal.valueOf(25)));       
ranges.add(new Range(BigDecimal.valueOf(26), BigDecimal.valueOf(50)));
ranges.add(new Range(BigDecimal.valueOf(51), BigDecimal.valueOf(75)));
ranges.add(new Range(BigDecimal.valueOf(76), BigDecimal.valueOf(100)));

Map<Range, Long> result = new HashMap<>();
ranges.forEach(r -> result.put(r, 0L)); // Add all ranges with a count of 0
input.values().forEach( // For each value in the map
        bd -> ranges.stream() 
            .filter(r -> r.inRange(bd)) // Find ranges it is in (can be in multiple)
            .forEach(r -> result.put(r, result.get(r) + 1)) // And increment their count
    );

System.out.println(result); // {51-75=1, 76-100=1, 26-50=0, 0-25=2}

I also had a solution with the groupingBy collector, but it was twice as big and couldn't deal with overlapping ranges or values that weren't in any range, so I think a solution like this will be better.

Upvotes: 2

KayV
KayV

Reputation: 13835

Here is the code which you can use:

    public static void groupByRange() {
        List<MyBigDecimal> bigDecimals = new ArrayList<MyBigDecimal>();
        for(int i =0; i<= 10; i++) {
            MyBigDecimal md = new MyBigDecimal();
            if(i>0 && i<= 2)
                md.setRange(1);
            else if(i>2 && i<= 5)
                md.setRange(2);
            else if(i>5 && i<= 7)
                md.setRange(3);
            else
                md.setRange(4);
            md.setValue(i);

            bigDecimals.add(md);
        }


        Map<Integer, List<MyBigDecimal>>  result = bigDecimals.stream()
                .collect(Collectors.groupingBy(e -> e.getRange(), 
                        Collector.of(
                                ArrayList :: new, 
                                (list, elem) -> { 
                                                        if (list.size() < 2) 
                                                            list.add(elem); 
                                                    }, 
                                (list1, list2) -> {
                                         list1.addAll(list2);
                                    return list1;
                                }
                           )));

        for(Entry<Integer, List<MyBigDecimal>> en : result.entrySet()) {
            int in = en.getKey();
            List<MyBigDecimal> cours  = en.getValue();
            System.out.println("Key Range = "+in + " , List Size : "+cours.size());
        }


    }


class MyBigDecimal{


    private int range;
    private int value;

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }

    public int getRange() {
        return range;
    }

    public void setRange(int range) {
        this.range = range;
    }

}

Upvotes: 0

Veselin Davidov
Veselin Davidov

Reputation: 7071

You can do it like that:

public class MainClass {
    public static void main(String[] args) {
        Map<String, BigDecimal> aMap=new HashMap<>();

        aMap.put("A",new BigDecimal(12));
        aMap.put("B",new BigDecimal(23));
        aMap.put("C",new BigDecimal(67));
        aMap.put("D",new BigDecimal(99));
         Map<String, Long> o =  aMap.entrySet().stream().collect(Collectors.groupingBy( a ->{
             //Do the logic here to return the group by function
             if(a.getValue().compareTo(new BigDecimal(0))>0 &&
                     a.getValue().compareTo(new BigDecimal(25))<0)
                 return "0-25";

             if(a.getValue().compareTo(new BigDecimal(26))>0 &&
                     a.getValue().compareTo(new BigDecimal(50))<0)
                 return "26-50";

             if(a.getValue().compareTo(new BigDecimal(51))>0 &&
                     a.getValue().compareTo(new BigDecimal(75))<0)
                 return "51-75";
             if(a.getValue().compareTo(new BigDecimal(76))>0 &&
                     a.getValue().compareTo(new BigDecimal(100))<0)
                 return "76-100";

             return "not-found";
         }, Collectors.counting()));

         System.out.print("Result="+o);


    }

}

Result is : Result={0-25=2, 76-100=1, 51-75=1}

I couldn't find a better way to do that check for big decimals but you can think about how to improve it :) Maybe make an external method that does that trick

Upvotes: 6

Related Questions