Find number of elements in range from map object - java

Map structure and data is given below
Map<String, BigDecimal>
A, 12
B, 23
C, 67
D, 99
Now i want to group values in range, output has range as key and number of elements there as value. Like below:
0-25, 2
26-50, 0
51-75, 1
76-100, 1
How can we do this using java streams ?

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

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.

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.

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

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);

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;
}
}

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

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()))

Related

Promotion progression logic

i have list of items in cart(assume each letter is an item)
cart list = a,s,d,f,a,s,d,f,a
here is the promotion
buy 1 a and get 2 d Free of Charge(in the below logic 1 is srcNum and 2 is tarNum)
the logic should be progressive.(for each a 2d should be free).
for the above input o/p should be d,d
i made some thing like below. but not working
any help appreciated
Iterator tempIterator = tempList.iterator();
boolean targetCheck = false;
int check=0;
boolean runCompleted = false;
while (!runCompleted && tempIterator.hasNext()){
String itemCode = (String) tempIterator.next();
if(!targetCheck && targetItemsList.contains(itemCode) && check < tarNum){
tempIterator.remove();
check++;
}
else if (check >= tarNum && targetCheck == false) {
check = 0;
targetCheck = true;
}
else if (check < srcNum && targetCheck == true) {
tempIterator.remove();
Integer discountQuantity = discountedItems.get(itemCode);
if(null==discountQuantity) {
discountQuantity = 1;
}else {
discountQuantity++;
}
discountedItems.put(itemCode,discountQuantity);
check++;
}
else if (check >= srcNum && targetCheck == true) {
check = 0;
targetCheck = false;
}
if(tempList.size()==0){
runCompleted = true;
}else{
tempIterator = tempIterator = tempList.iterator();
}
Your discount must be stored: item a has 2 d free. Since java 9 you can use the record class.
record Discount(int itemCode, int free) {};
Map<Integer, Discount> itemCodeToDiscountMap = new HashMap<>();
This becomes a bit more complex if 2 a 1 d free or even 2 a 1 a free. But not unduly.
You have a chaotic cart, something like:
List<Item> cartList = new ArrayList<>();
This is best kept in a map of item code to quantity.
Map<Integer, Integer> itemCodeToQuantityMap = new HashMap<>();
At the end of your evaluation you will have:
Map<Integer, Integer> itemsToPay = new HashSet<>();
Map<Integer, Integer> itemsFree = new HashSet<>();
Map<Integer, Integer> itemsCouldTakeFree = new HashSet<>();
So [a, a, b, d, d, d] with 1a=2d free:
itemsToPay = [a -> 2, b -> 1]
itemsFree = [d -> 3]
itemsCouldTakeFree = [d -> 1] "You can fetch 1 d free"
The first step, simplifying the cart data:
List<Item> cartList = new ArrayList<>();
...
Map<Integer, Integer> itemCodeToQuantityMap = new HashMap<>();
for (Item item: cartList) {
Item oldItem = itemCodeToQuantityMap.get(item.itemCode);
...
itemCodeToQuantityMap.get(item.itemCode, ...);
}
And then
itemsToPay.putAll(itemCodeToQuantityMap);
for (Map.Entry<Integer, Integer> entry: itemCodeToQuantityMap.entrySet()) {
int itemCode = entry.getKey();
int quantity = entry.getValue();
Discount discount = itemCodeToDiscountMap.get(itemCode);
...
}
First making a copy of itemCodeToQuantityMap into itemsToPay means you need not alter itemCodeToQuantityMap but can discount in / subtract from itemsToPay.
As this reeks of home work, I leave it at that. Just the tips:
Use data structures easying the work; here having the quantity of every item.
So one needs a Map.

How to change pairs in a map?

I have a Map<Integer, Character> alphabetMap, which contains links from number to alphabet.
For example: [1: 'A', 2: 'B', ... , 26: 'Z']
I have a rotate() method, which should put the entries with changing links
After first using of the method my map should be [1: 'Z', 2: 'A', 3: 'B', ... , 26: 'Y']
Here is my current realisation:
public void rotate() {
final Map<Integer, Character> tempMap = new HashMap<>();
alphabetMap.forEach((key, value) -> tempMap.put(key == 26 ? 1 : key + 1, value));
alphabetMap = tempMap;
}
Is there any another way/algorhitm to "rotate" my entries quicker?
For "faster" rotation the map could be replaced with a list and then method Collections.rotate could be used for this purpose. Then the list elements may be accessed by index in range [0..25].
Or a small wrapper class may be implemented:
static class MyCharMap {
private List<Character> chars = IntStream
.rangeClosed('A', 'Z')
.mapToObj(c -> (char)c)
.collect(Collectors.toList());
public void rotate() {
Collections.rotate(chars, 1);
}
public Character get(Integer i) {
assert(1 <= i && i <= 26);
return chars.get(i - 1);
}
#Override
public String toString() {
StringBuilder sb = new StringBuilder(4 * chars.size() + 2);
sb.append('{');
for (int i = 0, n = chars.size(); i < n; i++) {
if (i > 0) sb.append(", ");
sb.append(i + 1).append(':').append(chars.get(i));
}
sb.append('}');
return sb.toString();
}
}
Test:
MyCharMap chars = new MyCharMap();
chars.rotate();
chars.rotate();
System.out.println(chars);
System.out.println(chars.get(1));
Output:
{1:Y, 2:Z, 3:A, 4:B, 5:C, 6:D, 7:E, 8:F, 9:G, 10:H, 11:I, 12:J, 13:K, 14:L, 15:M, 16:N, 17:O, 18:P, 19:Q, 20:R, 21:S, 22:T, 23:U, 24:V, 25:W, 26:X}
Y
There is nothing wrong with your approach if using a map. But since rotations must be relative to some order and maps are unordered, you may want to use a List as suggested by Alex Rudenko.
Here is another alternative using maps. It permits left or right rotation by any amount (based on the sign) for a supplied map that has sequential integer keys starting at 1. It also adjusts for counts exceeding the size by using the remainder operator. For left or right rotation offsets are simply calculated and the map altered and returned for subsequent processing.
BiFunction<Map<Integer, Character>, Integer, Map<Integer, Character>> rotate =
(mp, cnt) -> {
int size = mp.values().size();
int count = cnt < 0 ? size + (cnt % size) - 1 :
cnt - 1;
return mp.entrySet().stream()
.map(e -> new AbstractMap.SimpleEntry<>(
(e.getKey() + count) % size + 1,
e.getValue()))
.collect(Collectors.toMap(e -> e.getKey(),
e -> e.getValue()));
};
System.out.println(map); // original map - 10 elements
map = rotate.apply(map,1); // right one - starting at J
System.out.println(map);
map = rotate.apply(map,-2); // left two, skipping A, going to B
System.out.println(map);
map = rotate.apply(map, -21);// Essentially left one going to C
System.out.println(map);
map = rotate.apply(map, 22); // Essentially right two going to A
System.out.println(map);
prints
{1=A, 2=B, 3=C, 4=D, 5=E, 6=F, 7=G, 8=H, 9=I, 10=J}
{1=J, 2=A, 3=B, 4=C, 5=D, 6=E, 7=F, 8=G, 9=H, 10=I}
{1=B, 2=C, 3=D, 4=E, 5=F, 6=G, 7=H, 8=I, 9=J, 10=A}
{1=C, 2=D, 3=E, 4=F, 5=G, 6=H, 7=I, 8=J, 9=A, 10=B}
{1=A, 2=B, 3=C, 4=D, 5=E, 6=F, 7=G, 8=H, 9=I, 10=J}
The lambda version could easily be replaced by a regular method that takes a single rotate value and works on a fixed map.
As stated by #code-apprentice in a comment
Since the keys are in numerical order, I suggest using an array or a
list instead of a map.
If in case you still want to return a map, you could use the Collections.rotate(...) method in conjunction with reconstructing the expected map.
BiFunction<List<?>, Integer, Map<Integer, ?>> rotate =
(list, distance) -> {
Function<List<?>, Map<Integer, ?>> setMap = (arrayList) -> IntStream.range(0, arrayList.size())
.boxed()
.collect(Collectors.toMap(arrayList::get, Function.identity()))
.entrySet()
.stream()
.peek(e -> e.setValue(e.getValue() + 1))
.collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey));
Collections.rotate(list, distance);
return setMap.apply(list);
};
System.out.println(rotate.apply(Arrays.asList('A', 'B', 'C', 'D'), 1)); // {1=D, 2=A, 3=B, 4=C}
System.out.println(rotate.apply(Arrays.asList("Peter", "James", "Sam", "Tiffany", "Mathew"), -3)); // {1=Tiffany, 2=Mathew, 3=Peter, 4=James, 5=Sam}
Based on Alex Rudenko's answer, you can create a custom map that uses Collections.rotate(...) behind the scenes.
Custom map:
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
class MyMap {
private static int counter = 0;
private final Map<Integer, ?> initMap;
private final List<?> list;
private Map<Integer, ?> rotatedMap;
public MyMap(List<?> list) {
this.list = list;
this.initMap = setMap(this.list);
this.rotatedMap = cloneMap(this.initMap);
}
private Map<Integer, ?> setMap(List<?> list) {
return IntStream.range(0, list.size())
.boxed()
.collect(Collectors.toMap(list::get, Function.identity()))
.entrySet()
.stream()
.peek(e -> e.setValue(e.getValue() + 1))
.collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey));
}
private Map<Integer, ?> cloneMap(Map<Integer, ?> map) {
return map.entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
public Map<Integer, ?> get() {
counter = 0;
return rotatedMap;
}
public MyMap reset() {
counter = 0;
this.rotatedMap = cloneMap(this.initMap);
return this;
}
public MyMap rotate() {
return rotateMap(++counter);
}
public MyMap rotate(int distance) {
return rotateMap(distance);
}
private List<?> cloneList(List<?> list) {
return list.stream().map(e -> e).collect(Collectors.toList());
}
private MyMap rotateMap(int distance) {
List<?> list = cloneList(this.list);
Collections.rotate(list, distance);
this.rotatedMap = setMap(list);
return this;
}
public String toString() {
return rotatedMap.toString();
}
}
Example 1 (Rotate twice, Reset rotation, Rotate twice.):
// Rotate twice, Reset rotation, Rotate twice.
MyMap myCharMap1 = new MyMap(Arrays.asList('A', 'B', 'C', 'D'));
System.out.println(myCharMap1
.rotate(2)
.reset()
.rotate().rotate()
.get()); // {1=C, 2=D, 3=A, 4=B}
Example 2 (Rotate thrice. Can rotate a String list):
//Rotate thrice.
MyMap myMap = new MyMap(Arrays.asList("Peter", "James", "Sam", "Tiffany", "Mathew"));
System.out.println(myMap.rotate(3).get()); // {1=Sam, 2=Tiffany, 3=Mathew, 4=Peter, 5=James}
Example 3 (Rotate once. Can rotate a numeric list as well):
// Rotate once. Can rotate a numeric list as well.
MyMap myDigitMap3 = new MyMap(Arrays.asList(38, 56, 98, 160));
System.out.println(myDigitMap3.rotate(1).get()); // {1=160, 2=38, 3=56, 4=98}
Example 4 (Can accept negative rotations):
//Can accept negative rotations.
MyMap myMap = new MyMap(Arrays.asList("Peter", "James", "Sam", "Tiffany", "Mathew"));
System.out.println(myMap.rotate(-3).get()); // {1=Tiffany, 2=Mathew, 3=Peter, 4=James, 5=Sam}

How to print the sum of values from the same objects of 2 parameter in array

I am learning java and I hit into a snag as I could not figure out my loop or array.
I have an array which contains class objects containing a string and integer parameters, in my code, it will be name and dollars.
I am trying to print out the array in which, if there is a same name, it is to print once and with the sum of the dollars (from the same name).
In my Dollars.java
public class Dollars
{
private String name;
private int dollars;
public dollars (String name, int dollars)
{
this.name = name;
this.dollars = dollars;
}
public String getName()
{
return name;
}
public int getChange()
{
return dollars;
}
}
In my main file/ TestDollars.java
public class TestDollars
{
public static void displayArray(Dollars[] dol)
{
int sum = 0;
for (int n=0; n<dol.length; n++)
{
for (int m=n+1; m<dol.length; m++)
{
if (dol[n].getName().equals(dol[m].getName()))
{
// System.out.printf("%s -- %d\n", dol[n].getName(), dol[n].getChange());
sum = dol[n].getChange() + dol[m].getChange();
System.out.printf("%s -- %d\n", dol[m].getName(), sum);
break;
}
}
System.out.printf("%s -- %d\n", dol[n].getName(), dol[n].getChange());
}
}
public static void main(String[] args)
{
// Test with 5 records
Dollars[] dollarsArr = new Dollars[5];
dollarsArr[0] = new Dollars("john", 10);
dollarsArr[1] = new Dollars("peter", 12);
dollarsArr[2] = new Dollars("sam", 5);
dollarsArr[3] = new Dollars("alvin", 16);
dollarsArr[4] = new Dollars("peter", 30);
displayArray(dollarsArr);
}
}
Irregardless where I place my print statement in the displayArray, the record 'peter' will gets printed twice.
Expected output:
john -- 10
peter -- 42
sam -- 5
alvin -- 16
Current output:
john -- 10
peter -- 42
peter -- 12
sam -- 5
alvin -- 16
peter -- 30
You want to group your list by name, please use JAVA 8+ API Stream and the collector group by
public static void displayArray(Dollars[] dol)
{
Stream.of(dol)
// Group by name
.collect(Collectors.groupingBy(Dollars::getName))
.entrySet().stream()
// Collect a map name and calculate the sum
.collect(
Collectors.toMap(x -> {
int total= x.getValue().stream().mapToInt(Dollars::getChange).sum();
return new Dollars(x.getKey(),total);
}, Map.Entry::getValue))
// Print
.forEach((dollarsTotal, vals) -> {
System.out.println(dollarsTotal.getName()+ " -- "+ dollarsTotal.getChange());
// Bonus : Display transactions :
for(Dollars transaction : vals)
{
System.out.println(" \t "+transaction.getName() + " add -- " + transaction.getChange());
}
});
}
If you want only the values you can collect the keyset
Set<Dollars> groupedByName = Stream.of(dol)
// Group by name
.collect(Collectors.groupingBy(Dollars::getName))
.entrySet().stream()
// Collect a map name and calculate the sum
.collect(
Collectors.toMap(x -> {
int total= x.getValue().stream().mapToInt(Dollars::getChange).sum();
return new Dollars(x.getKey(),total);
}, Map.Entry::getValue)).keySet();
The other answer guides you on fixing your code (Buy they require more work to avoid double counting).
You can reduce the time complexity from O(n2) to O(n) (and make it simpler) by having a data structure (like a map) to aggregate the result.
Let us create a Map<String, Integer> to map a name to the total dollars for that name.
Map<String, Integer> result = new HashMap<>();
for (int i = 0; i < dol.length; i++) {
if (!result.containsKey(dol[i].getName())) { //first time you encounter a name
result.put(dol[i].getName(), dol[i].getChange());
} else {
//add the current change to the already existing sum
int sumSoFar= result.get(dol[i].getName());
result.put(dol[i].getName(), sumSoFar + dol[i].getChange());
}
}
System.out.println(result);
Result is,
{peter=42, alvin=16, john=10, sam=5}
You can simplify the above code using Map's merge method as:
for (Dollars dollars : dol) {
result.merge(dollars.getName(), dollars.getChange(), Integer::sum);
}
The third argument is a BiFunction which sums up the old value and new value (the sum accumulated so far and the current change value). When written as a lambda expression, Integer::sum can be written as (sumSoFar, currentChange) -> sumSoFar + currentChange.
A stream evangelist way would be to use Collectors.groupingBy and Collectors.summingInt.
Arrays.stream(dol)
.collect(Collectors.groupingBy(Dollars::getName, Collectors.summingInt(Dollars::getChange)));
While navigating the array, assign the change of each non-null array-element to a variable e.g. sum and then add the change of the succeeding duplicate elements to it. Make sure to assign null to the indices where duplicate elements are found so that they can not be counted again. It also means that you will have to perform a null check before performing any operation on the array elements. Print the value of sum once you have checked the complete array for duplicate elements.
public static void displayArray(Dollars[] dol) {
for (int n = 0; n < dol.length; n++) {
if (dol[n] != null) {
int sum = dol[n].getChange();
for (int m = n + 1; m < dol.length; m++) {
if (dol[m] != null && dol[n].getName().equals(dol[m].getName())) {
sum += dol[m].getChange();
dol[m] = null;
}
}
System.out.printf("%s -- %d\n", dol[n].getName(), sum);
}
}
}
Output:
john -- 10
peter -- 42
sam -- 5
alvin -- 16
Note: If you want to keep the original array intact, pass the clone of the array to the method, displayArray instead of passing the array itself as shown below:
displayArray(dollarsArr.clone());

Using Java streams grouping the list data based on the date intervals and sum the amount field

Consider there is a class : Partner
Class Partner {
LocalDate invoiceDate;
BigDecimal amount;
}
Now, I have a list of Partner objects sorted in descending order eg:
[Partner(invoiceDate=2020-01-21, amount=400),
Partner(invoiceDate=2020-01-20, amount=400),
Partner(invoiceDate=2020-01-11, amount=400),
Partner(invoiceDate=2020-01-10, amount=400),
.....,
.....]
In the above sample data the field "invoiceDate" is for January month.
note: the list will have data for 12 months or above.
Now,
I want to group the data in 15 days interval .i.e.
First interval, 1st day to 15th day of the month [1 - 15].
Second interval, 16th day to Last day of the month [16 - 30/31/28/29].
And finally sum the amount value between the date range.
Calculation form the above sample data :
first interval [1-15] : 2 rows qualifies => [invoiceDate=2020-01-11 and invoiceDate=2020-01-10]
second interval [16-31] : 2 rows qualifies => [invoiceDate=2020-01-21 and invoiceDate=2020-01-20]
The final output data should look like this :
[Partner(invoiceDate=2020-01-31, amount=800),
Partner(invoiceDate=2020-01-15, amount=800)]
note : In the final output invoiceDate should be the last day of the interval.
Using groupingBy would help in such a situation. Here is one of the approaches:
import static java.util.stream.Collectors.*;
Map<String, BigDecimal> dateRangeToSumMap = list
.stream()
.collect(
groupingBy(e -> e.invoiceDate.getDayOfMonth() > 15 ? "16-31" : "1-16",
mapping(
Partner::getAmount,
reducing(BigDecimal.ZERO, BigDecimal::add)
)
)
);
// iterate the map to get the output
dateRangeToSumMap.forEach((k, v) -> {
System.out.println("Date Range = " + k + " , Sum = " + v);
});
Output:
Date Range = 1-16 , Sum = 800
Date Range = 16-31 , Sum = 800
The final output data should look like this :
[Partner(invoiceDate=2020-01-31, amount=800),
Partner(invoiceDate=2020-01-15, amount=800)]
This can be constructed with the map we have.
note : In the final output invoiceDate should be the last day of the interval.
With year and month, we can get the correct last day of the interval.
I think this should give you at least something to start with:
Function<Partner, LocalDate> getInterval = (partner) -> {
// compute if day is before 15th of month and return either 15th
// of month or last day of month
return null;
};
Collection<Partner> partners = Arrays.asList();
Map<LocalDate, BigDecimal> result = partners.stream()
.collect(Collectors.toMap(getInterval, Partner::getAmount, (a1, a2) -> a1.add(a2)));
You can use the map collector with the merge function to get the sum for all elements with the same key.
Java's verbosity and lack of a tuple type makes the code somewhat longer, this can be done in one pass without any grouping.
public class Main {
public static void main(String[] args) {
List<Partner> partners = Arrays.asList(
new Partner(LocalDate.of(2020, 1, 21), BigDecimal.valueOf(400)),
new Partner(LocalDate.of(2020, 1, 20), BigDecimal.valueOf(400)),
new Partner(LocalDate.of(2020, 1, 11), BigDecimal.valueOf(400)),
new Partner(LocalDate.of(2020, 1, 10), BigDecimal.valueOf(400))
);
Accumulator results = IntStream
.range(0, partners.size())
.mapToObj(i -> new AbstractMap.SimpleImmutableEntry<>(i, partners.get(i)))
.reduce(new Accumulator(), (Accumulator acc, Map.Entry<Integer, Partner> e) -> {
Partner p = e.getValue();
if (p.invoiceDate.getMonthValue() == acc.month || e.getKey() == 0) {
BiWeeklyReport bucket = p.invoiceDate.getDayOfMonth() <= 15 ? acc.first : acc.second;
bucket.amount = bucket.amount.add(p.amount);
bucket.invoiceDate = bucket.invoiceDate.isAfter(p.invoiceDate) ? bucket.invoiceDate : p.invoiceDate;
acc.month = bucket.invoiceDate.getMonthValue();
}
if (e.getKey() == partners.size() - 1 || p.invoiceDate.getMonthValue() != acc.month) {
if (acc.first.amount.compareTo(BigDecimal.ZERO) > 0) {
acc.reports.add(acc.first);
}
if (acc.second.amount.compareTo(BigDecimal.ZERO) > 0) {
acc.reports.add(acc.second);
}
acc.first = new BiWeeklyReport();
acc.second = new BiWeeklyReport();
}
return acc;
}, (acc1, acc2) -> {
acc1.reports.addAll(acc2.reports);
return acc1;
});
System.out.println(results.reports);
}
private static class Partner {
LocalDate invoiceDate;
BigDecimal amount;
private Partner(LocalDate invoiceDate, BigDecimal amount) {
this.invoiceDate = invoiceDate;
this.amount = amount;
}
}
private static class BiWeeklyReport {
BigDecimal amount = BigDecimal.ZERO;
LocalDate invoiceDate = LocalDate.of(1970, 1, 1);
#Override
public String toString() {
return "BiWeeklyReport{" +
"amount=" + amount.longValue() +
", invoiceDate=" + DateTimeFormatter.ISO_LOCAL_DATE.format(invoiceDate) +
'}';
}
}
private static class Accumulator {
List<BiWeeklyReport> reports = new ArrayList<>();
BiWeeklyReport first = new BiWeeklyReport();
BiWeeklyReport second = new BiWeeklyReport();
int month = -1;
}
}
Output:
[BiWeeklyReport{amount=800, invoiceDate=2020-01-11}, BiWeeklyReport{amount=800, invoiceDate=2020-01-21}]
You can first map invoiceDate into 15-day interval date. Then use Collectors.toMap to map the data into for invoiceDate and sum the amount in merge function by creating new Parter object. Finally get the map values as list.
Map<LocalDate, Partner> result = list.stream()
.map(p -> new Partner(p.getInvoiceDate().getDayOfMonth() > 15
? p.getInvoiceDate().withDayOfMonth(p.getInvoiceDate().lengthOfMonth())
: p.getInvoiceDate().withDayOfMonth(15),
p.getAmount()))
.collect(Collectors.toMap(Partner::getInvoiceDate, e -> e,
(a, b) -> new Partner(a.getInvoiceDate(), a.getAmount().add(b.getAmount()))));
List<Partner> res = new ArrayList<>(result.values());
Another approach :
You can simplified the code without creating the Partner object the way #Naman suggested
static LocalDate getIntervalDate(LocalDate d) {
return (d.getDayOfMonth() > 15 ? d.withDayOfMonth(d.lengthOfMonth())
: d.withDayOfMonth(15));
}
List<Partner> res = list.stream()
.collect(Collectors.toMap(e -> getIntervalDate(e.getInvoiceDate()),
e -> e.getAmount(), (a, b) -> a.add(b)))
.entrySet()
.stream()
.map(e -> new Partner(e.getKey(), e.getValue()))
.collect(Collectors.toList());

Optimizing Opportunities with Java Streams

I was looking through some code and came across this method that takes an HTML Header value (i.e. Content-Disposition=inline;filename=foo.bar) and parses it into a map separated by the semi-colon's into key=value pairs. At first it looked like a good candidate for optimization using a stream, but after I implemented it, the fact that I can't reuse the computed String.indexOf('=') value means the string must be scanned 3 times, which is actually less optimal than the original. I'm perfectly aware that there are many instances where Streams aren't the right tool for the job, but I was wondering if I had just missed some technique that could allow the Stream to be as performant/more performant than the initial code.
/**
* Convert a Header Value String into a Map
*
* #param value The Header Value
* #return The data Map
*/
private static Map<String,String> headerMap (String value) {
int eq;
Map<String,String> map = new HashMap<>();
for(String entry : value.split(";")) {
if((eq = entry.indexOf('=')) != -1) {
map.put(entry.substring(0,eq),entry.substring(eq + 1));
}
}
return map;
return Stream.of(value.split(";")).filter(entry -> entry.indexOf('=') != -1).collect(Collectors.));
} //headerMap
My attempt at Streaming it:
/**
* Convert a Header Value String into a Map
*
* #param value The Header Value
* #return The data Map
*/
private static Map<String,String> headerMap (String value) {
return Stream.of(value.split(";")).filter(entry -> entry.indexOf('=') != -1).collect(Collectors.toMap(entry -> entry.substring(0,entry.indexOf('=')),entry -> entry.substring(entry.substring(entry.indexOf('=') + 1))));
} //headerMap
This solution looks for '=' only once:
private static Map<String, String> headerMap(String value) {
return Stream.of(value.split(";"))
.map(s -> s.split("=", 2))
.filter(arr -> arr.length == 2)
.collect(Collectors.toMap(arr -> arr[0], arr -> arr[1]));
}
Note that here the fast-path for String.split is used, thus regular expression is not actually created.
Note that using Guava you can do this in quite clean way even prior to Java-8:
private static Map<String, String> headerMap(String value) {
return Splitter.on( ';' ).withKeyValueSeparator( '=' ).split( value );
}
In general I would advise you against manual parsing of HTTP headers. There are many caveats there. See, for example, how it's implemented in Apache HTTP library. Use libraries.
I came up with the following code:
private static Map<String, String> headerMap(String value) {
return Stream.of(value.split(";"))
.filter(entry -> entry.indexOf('=') != -1)
.map(entry -> {
int i = entry.indexOf('=');
return new String[] { entry.substring(0, i), entry.substring(i + 1) };
})
.collect(Collectors.toMap(array -> array[0], array -> array[1]));
}
It only scans for the entry two times, by storing the key and value inside an array of size 2. I'm not sure it will be as performant as the for loop since we are creating another Object to serve just as a holder.
Another solution that scans the entry only one time is this, although I'm not very found of it:
private static Map<String, String> headerMap(String value) {
return Stream.of(value.split(";"))
.map(entry -> {
int i = entry.indexOf('=');
if (i == -1) {
return null;
}
return new String[] { entry.substring(0, i), entry.substring(i + 1) };
})
.filter(Objects::nonNull)
.collect(Collectors.toMap(array -> array[0], array -> array[1]));
}
I realized a JMH benchmark to test this. Following is the benchmark code:
#Warmup(iterations = 5, time = 1000, timeUnit = TimeUnit.MILLISECONDS)
#Measurement(iterations = 10, time = 1000, timeUnit = TimeUnit.MILLISECONDS)
#BenchmarkMode(Mode.AverageTime)
#OutputTimeUnit(TimeUnit.MICROSECONDS)
#Fork(3)
#State(Scope.Benchmark)
public class StreamTest {
private static final String VALUE = "Accept=text/plain;"
+ "Accept-Charset=utf-8;"
+ "Accept-Encoding=gzip, deflate;"
+ "Accept-Language=en-US;"
+ "Accept-Datetime=Thu, 31 May 2007 20:35:00 GMT;"
+ "Cache-Control=no-cache;"
+ "Connection=keep-alive;"
+ "Content-Length=348;"
+ "Content-Type=application/x-www-form-urlencoded;"
+ "Date=Tue, 15 Nov 1994 08:12:31 GMT;"
+ "Expect=100-continue;"
+ "Max-Forwards=10;"
+ "Pragma=no-cache";
#Benchmark
public void loop() {
int eq;
Map<String, String> map = new HashMap<>();
for (String entry : VALUE.split(";")) {
if ((eq = entry.indexOf('=')) != -1) {
map.put(entry.substring(0, eq), entry.substring(eq + 1));
}
}
}
#Benchmark
public void stream1() {
Stream.of(VALUE.split(";"))
.filter(entry -> entry.indexOf('=') != -1)
.map(entry -> {
int i = entry.indexOf('=');
return new String[] { entry.substring(0, i), entry.substring(i + 1) };
})
.collect(Collectors.toMap(array -> array[0], array -> array[1]));
}
#Benchmark
public void stream2() {
Stream.of(VALUE.split(";"))
.map(entry -> {
int i = entry.indexOf('=');
if (i == -1) {
return null;
}
return new String[] { entry.substring(0, i), entry.substring(i + 1) };
})
.filter(Objects::nonNull)
.collect(Collectors.toMap(array -> array[0], array -> array[1]));
}
public static void main(String[] args) throws Exception {
Main.main(args);
}
}
and this is the result (Code i5 3230M CPU # 2.60 GHz, Windows 10, Oracle JDK 1.8.0_25):
Benchmark Mode Cnt Score Error Units
StreamTest.loop avgt 30 1,541 ± 0,038 us/op
StreamTest.stream1 avgt 30 1,633 ± 0,042 us/op
StreamTest.stream2 avgt 30 1,604 ± 0,058 us/op
What this demonstrates is that both the streams solution and the for loop are actually equivalent in terms of performance.

Categories

Resources