Reputation: 109
How to group by month based on date using java and calculate total count?
public static void main(String[] args) {
Map<String, Object>out=new HashMap<String, Object>();
Map<String,Object> hm=new HashMap<String, Object>();
List<String> al= new ArrayList<String>();
al.add("51b6f5fde4b0dd92df2c3270");
al.add("51b866e9e4b021170dd1ae1c");
hm.put("sDate","02-Oct-2015");
hm.put("status","S");
hm.put("SMSSentId", al);
out.put("Student1", hm);
Map<String,Object> hm1=new HashMap<String, Object>();
List<String> al1= new ArrayList<String>();
al1.add("51b6f5fde4b0dd92df2c3271");
al1.add("51b866e9e4b021170dd1ae12");
hm1.put("sDate","03-Oct-2015");
hm1.put("status","S");
hm1.put("SMSSentId", al1);
out.put("Student2", hm1);
Map<String,Object> hm2=new HashMap<String, Object>();
List<String> al2= new ArrayList<String>();
al2.add("51b6f5fde4b0dd92df2c3271");
hm2.put("sDate","03-Oct-2016");//Year changed
hm2.put("status","S");
hm2.put("SMSSentId", al2);
out.put("Student3", hm2);
//System.out.println(out);
for (Map.Entry<String, Object> entry : out.entrySet())
{
// System.out.println(entry.getKey() + "/" + entry.getValue());
for (Map.Entry<String, Object> entry1 : hm.entrySet())
{
System.out.println(entry1.getKey() + "/" + entry1.getValue());
if(entry1.getKey().equals("SMSSentId"))
{
int a= ((List<String>) entry1.getValue()).size();
System.out.println(a);
}
}
}
}
I dont know how to modify this map and list.Please give me suggestion its a correct way or not or any other comparator method
i expected this output
# | Month | Year | TotalSMSSent
1 Oct 2015 4
2 Oct 2016 1
Upvotes: 0
Views: 5256
Reputation: 339342
record
, rather than using maps or parallel arrays/lists.SequencedMap < YearMonth, Integer > report =
messagesSent
.stream ( )
.collect (
Collectors.groupingBy (
( studentSentMessagesOnDate ) -> YearMonth.from ( studentSentMessagesOnDate.dateSent ) ,
TreeMap :: new , // A `Supplier` implementation that is our factory for a `SequencedMap` implementation instance.
Collectors.summingInt ( sent -> sent.messageIds.size ( ) )
)
);
Tip: A declaration using Object
may indicate a less than optimal design in your code.
Map<String, Object>
Read on for an alternative design.
You should use date-time types for your date rather than mere string like "03-Oct-2015"
. But use only the modern *java.time" classes defined in JSR 310, built into Java 8 and later.
Never use Date
, Calendar
, SimpleDateFormat
, or any of the other terribly-flawed legacy date-time classes.
LocalDate
For a date-only value, without time-of-day, and without time zone or offset-from-UTC, use the java.time.LocalDate
class.
DateTimeFormatter
Define a formatting pattern to match your localized input.
Pass a Locale
to explicitly specify the human language and cultural norms needed to parse localized values such as the name of the month.
String input = "03-Oct-2015" ;
Locale locale = Locale.of( "en" , "US" ) ;
DateTimeFormatter f = DateTimeFormatter.ofPattern( "dd-MMM-uuuu" ).withLocale( locale ) ;
LocalDate ld = LocalDate.parse( input , f ) ;
By the way, avoid using localized text when exchanging date-time values. Use only ISO 8601 standard formats. For a date, that format is YYYY-MM-DD.
The java.time classes use the standard formats by default when parsing/generating text. So no need to specify a formatting pattern.
LocalDate ld = LocalDate.parse( "2015-10-03" ) ; // No need to specify a formatting pattern when using standard ISO 8601 formats.
No need to use a collection of arrays or List
objects to carry each field value of an entity. Java is a fully object-oriented language, so use those features. Define a class to represent each kind of entity in your app.
Do not use a Map
when you know all the fields involved. Generally best to use a class instead, where you get type-safety, and where your code becomes more self-documenting. Use a Map
in this way only when the fields may vary or are unknown at compile-time.
In modern Java, use record
to write a class intended to transparently communicate shallowly-immutable data.
By the way, you seem to be using strings like xx
as an identifier. Consider instead using the standardized Universally Unique Identifier (UUID). Java provides the UUID
class for that purpose.
Or perhaps that string is specific to SMS messaging. If so, you should write a class to represent such an ID, a class that validates input strings.
record
You neglected to explain your business domain. Furthermore, your chosen names are generic, non-descriptive; better naming makes for self-documenting code.
Also, you confusingly repeated your example message IDs. When posting here make your examples simple but realistic. Repeating IDs inappropriately is confusing.
But I am guessing you are tracking a list of messages sent to a student on a certain date. You seem to be tracking one student per map, but you omitted any identifier for that particular student; I added one for clarity.
By the way, status codes such as S
should be defined as an enum. But let's ignore that for this example.
Note that a record
may be defined locally, or separately. (Ditto interface
& enum
in modern Java, by the way.)
record StudentSentMessagesOnDate(
int id , // I invented this field for consistency, to make better sense.
LocalDate dateSent , // Use the *java.time* classes to represent date-time values.
String status , // Really should be an enum, but not relevant to this Question.
SequencedCollection < String > messageIds // A `List` is one kind of `SequencedCollection`.
) { }
Some example data:
SequencedCollection < StudentSentMessagesOnDate > messagesSent =
List.of (
new StudentSentMessagesOnDate
(
1 ,
LocalDate.parse ( "03-Oct-2015" , dateFormatter ) ,
"S" ,
List.of ( "51b6f5fde4b0dd92df2c3271" , "51b866e9e4b021170dd1ae12" )
) ,
new StudentSentMessagesOnDate
(
2 ,
LocalDate.parse ( "03-Oct-2016" , dateFormatter ) , // Different year.
"S" ,
List.of ( "51b6f5fde4b0dd92dc37a123" )
) ,
new StudentSentMessagesOnDate
(
3 ,
LocalDate.parse ( "03-Oct-2015" , dateFormatter ) , // Different year.
"S" ,
List.of ( "51b6f5fde4b0dd92d7d4a773" )
)
);
As a step closer to your goal of a report by month, let's first build a Map
of LocalDate
to a count of messages sent. That would be a Map < LocalDate , Integer >
.
// Get a count of messages sent per date.
// This code is modified from code published 2022-02 by Manu Barriola on DZone.com at URL:
// https://dzone.com/articles/grouping-and-aggregations-with-java-streams
Map < LocalDate, Integer > report =
messagesSent
.stream ( )
.collect (
Collectors.groupingBy (
StudentSentMessagesOnDate :: dateSent ,
Collectors.summingInt ( sent -> sent.messageIds.size ( ) )
)
);
This works. We get the expected results of 1 and 3.
report.toString() = {2016-10-03=1, 2015-10-03=3}
Next we can change this a bit to keep the map entries sorted by date. Use SequencedMap
rather than just Map
, with an implementation of TreeMap
. We use this overload of groupingBy
to provide a middle argument, a method reference to create a concrete instance of SequencedMap
, TreeMap
in our case.
SequencedMap < LocalDate, Integer > report =
messagesSent
.stream ( )
.collect (
Collectors.groupingBy (
StudentSentMessagesOnDate :: dateSent ,
TreeMap :: new , // A `Supplier` implementation that is our factory for a `SequencedMap` implementation instance.
Collectors.summingInt ( sent -> sent.messageIds.size ( ) )
)
);
We get our expected results of 3 & 1, with 2015 coming before 2016.
report.toString() = {2015-10-03=3, 2016-10-03=1}
Now we need to modify this to summarize by year-month rather than by each date. To represent a specific year-month, use YearMonth
class. You can extract an instance of YearMonth
from a LocalDate
object.
YearMonth ym = YearMonth.from( LocalDate.parse( "2015-10-03" ) ) ;
ym.toString() = 2015-10
First step in our modification is to declare our resulting SequencedMap
using YearMonth
as the key rather than LocalDate
.
SequencedMap < YearMonth, Integer > report = …
And second step is change replace the first argument to groupingBy
with code to extract a YearMonth
from each of our objects’ dateSent
field.
SequencedMap < YearMonth, Integer > report =
messagesSent
.stream ( )
.collect (
Collectors.groupingBy (
( studentSentMessagesOnDate ) -> YearMonth.from ( studentSentMessagesOnDate.dateSent ) ,
TreeMap :: new , // A `Supplier` implementation that is our factory for a `SequencedMap` implementation instance.
Collectors.summingInt ( sent -> sent.messageIds.size ( ) )
)
);
When run, we get the results we expect, 3 & 1 for two year-month values.
report.toString() = {2015-10=3, 2016-10=1}
Here is the entire app’s code for your copy-past convenience.
package work.basil.example.recs;
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
public class StudentTracking
{
public static void main ( String[] args )
{
StudentTracking app = new StudentTracking ( );
app.demo ( );
}
private void demo ( )
{
final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern ( "dd-MMM-uuuu" ).withLocale ( Locale.of ( "en" , "US" ) );
record StudentSentMessagesOnDate(
int id , // I invented this field for consistency, to make better sense.
LocalDate dateSent , // Use the *java.time* classes to represent date-time values.
String status , // Really should be an enum, but not relevant to this Question.
SequencedCollection < String > messageIds // A `List` is one kind of `SequencedCollection`.
) { }
SequencedCollection < StudentSentMessagesOnDate > messagesSent =
List.of (
new StudentSentMessagesOnDate
(
1 ,
LocalDate.parse ( "03-Oct-2015" , dateFormatter ) ,
"S" ,
List.of ( "51b6f5fde4b0dd92df2c3271" , "51b866e9e4b021170dd1ae12" )
) ,
new StudentSentMessagesOnDate
(
2 ,
LocalDate.parse ( "03-Oct-2016" , dateFormatter ) , // Different year.
"S" ,
List.of ( "51b6f5fde4b0dd92dc37a123" )
) ,
new StudentSentMessagesOnDate
(
3 ,
LocalDate.parse ( "03-Oct-2015" , dateFormatter ) , // Different year.
"S" ,
List.of ( "51b6f5fde4b0dd92d7d4a773" )
)
);
// Get a count of messages sent per date.
// This code is modified from code published 2022-02 by Manu Barriola on DZone.com at URL:
// https://dzone.com/articles/grouping-and-aggregations-with-java-streams
SequencedMap < YearMonth, Integer > report =
messagesSent
.stream ( )
.collect (
Collectors.groupingBy (
( studentSentMessagesOnDate ) -> YearMonth.from ( studentSentMessagesOnDate.dateSent ) ,
TreeMap :: new , // A `Supplier` implementation that is our factory for a `SequencedMap` implementation instance.
Collectors.summingInt ( sent -> sent.messageIds.size ( ) )
)
);
System.out.println ( "report.toString() = " + report );
}
}
Upvotes: 1
Reputation: 112
I have done some modifications to your code hope it will help.I have created a new class records called Records to hold all your records.
I have assumed that your date are in string form since you are not using Java Date
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Sorted {
public static void main(String[] args) {
Map<String, Records> out = new HashMap<String, Records>();
List<String> al = new ArrayList<String>();
al.add("51b6f5fde4b0dd92df2c3270");
al.add("51b866e9e4b021170dd1ae1c");
Records record = new Records("02-Oct-2015", "S", al);
out.put("student1", record);
al = new ArrayList<String>();
al.add("51b6f5fde4b0dd92df2c3271");
al.add("51b866e9e4b021170dd1ae12");
record = new Records("03-Oct-2015", "S", al);
out.put("Student2", record);
al = new ArrayList<String>();
al.add("51b6f5fde4b0dd92df2c3271");
record = new Records("03-Oct-2016", "S", al);
out.put("Student3", record);
process(out);
}
public static void process(Map<String, Records> records) {
HashMap<String, HashMap<String, Integer>> m = new HashMap<String, HashMap<String, Integer>>();
HashMap<String, Integer> month = null;
for (String recordKey : records.keySet()) {
Records r = records.get(recordKey);
String s[] = r.getsDate().split("-");
if (!m.containsKey(s[2])) {
month = new HashMap<String, Integer>();
m.put(s[2], month);
}
HashMap<String, Integer> m1 = m.get(s[2]);
if (!m1.containsKey(s[1])) {
m1.put(s[1], records.get(recordKey).getSMSSent().size());
} else {
int flag = m1.get(s[1]);
m1.put(s[1], flag + records.get(recordKey).getSMSSent().size());
}
}
display(m);
}
public static void display(HashMap<String, HashMap<String, Integer>> d) {
int i = 0;
for (String s : d.keySet()) {
Map<String, Integer> m = d.get(s);
for (String s1 : m.keySet()) {
System.out.print(i++);
System.out.print("\t");
System.out.print(s);
System.out.print("\t");
System.out.print(s1 + "\t" + m.get(s1));
System.out.println("");
}
}
}
}
class Records {
private String sDate;
private String status;
private List<String> SMSSent;
public Records(String sDate, String status, List<String> sMSSent) {
super();
this.sDate = sDate;
this.status = status;
SMSSent = sMSSent;
}
public String getsDate() {
return sDate;
}
public void setsDate(String sDate) {
this.sDate = sDate;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public List<String> getSMSSent() {
return SMSSent;
}
public void setSMSSent(List<String> sMSSent) {
SMSSent = sMSSent;
}
}
The Output Will be :
0 2016 Oct 1
1 2015 Oct 4
modify the code according to your requirement.
Upvotes: 1
Reputation: 2668
Please see below as an idea.
final SimpleDateFormat dateFormat = new SimpleDateFormat("dd-MMM-yyyy"); Map<String, Object> out=new HashMap<String, Object>(); Map<String,Object> hm=new HashMap<String, Object>(); List<String> al= new ArrayList<String>(); al.add("51b6f5fde4b0dd92df2c3270"); al.add("51b866e9e4b021170dd1ae1c"); hm.put("sDate","02-Oct-2015"); hm.put("status","S"); hm.put("SMSSentId", al); out.put("Student1", hm); Map<String,Object> hm1=new HashMap<String, Object>(); List<String> al1= new ArrayList<String>(); al1.add("51b6f5fde4b0dd92df2c3271"); al1.add("51b866e9e4b021170dd1ae12"); hm1.put("sDate","03-Oct-2015"); hm1.put("status","S"); hm1.put("SMSSentId", al1); out.put("Student2", hm1); Map<String,Object> hm2=new HashMap<String, Object>(); List<String> al2= new ArrayList<String>(); al2.add("51b6f5fde4b0dd92df2c3271"); hm2.put("sDate","03-Oct-2016");//Year changed hm2.put("status","S"); hm2.put("SMSSentId", al2); out.put("Student3", hm2); List<Integer> years = Arrays.asList(2015,2016); List<Integer> months = Arrays.asList(1,2,3,4,5,6,7,8,9,10,11,12); Map<String, Integer> report = new LinkedHashMap<String, Integer>(); for (Integer year : years) { for (Integer month : months) { Integer smsCount = 0; // loop on Students for (Map.Entry<String, Object> outEntry : out.entrySet()) { Map studentData = (Map)outEntry.getValue(); String sentDateAsString = (String)studentData.get("sDate"); Date sentDate = dateFormat.parse(sentDateAsString); Calendar cal = Calendar.getInstance(); cal.setTime(sentDate); if (cal.get(Calendar.MONTH) == (month - 1) && cal.get(Calendar.YEAR) == year) { List smsList = (List)studentData.get("SMSSentId"); smsCount += smsList.size(); } } report.put(String.format("Month %d-Year %d", month, year), smsCount); } } System.out.println(report.toString());
The output will be as below (Note, I have not formatted the output, I just print out the Report hashmap).
{Month 1-Year 2015=0, Month 2-Year 2015=0, Month 3-Year 2015=0, Month 4-Year 2015=0, Month 5-Year 2015=0, Month 6-Year 2015=0, Month 7-Year 2015=0, Month 8-Year 2015=0, Month 9-Year 2015=0, Month 10-Year 2015=4, Month 11-Year 2015=0, Month 12-Year 2015=0, Month 1-Year 2016=0, Month 2-Year 2016=0, Month 3-Year 2016=0, Month 4-Year 2016=0, Month 5-Year 2016=0, Month 6-Year 2016=0, Month 7-Year 2016=0, Month 8-Year 2016=0, Month 9-Year 2016=0, Month 10-Year 2016=1, Month 11-Year 2016=0, Month 12-Year 2016=0}
You can customize the code as much as you can achieve your purpose. Hope it helps.
Upvotes: 1