Aravi S
Aravi S

Reputation: 109

How to group by month based on date using java and calculate total count?

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

Answers (3)

Basil Bourque
Basil Bourque

Reputation: 339342

tl;dr

  • Use java.time for date-time values.
  • Define your domain entity as a class, likely a record, rather than using maps or parallel arrays/lists.
  • Use streams to process collections.
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 ( ) )
                        )
                );

Design

Tip: A declaration using Object may indicate a less than optimal design in your code.

Map<String, Object>

Read on for an alternative design.

java.time

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.

Avoid legacy date-time classes

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

ISO 8601

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.

Class

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.

Records

In modern Java, use record to write a class intended to transparently communicate shallowly-immutable data.

UUID

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.

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

Entire code

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

adhikari
adhikari

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

Nghia Do
Nghia Do

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

Related Questions