Java 根据列日期对 csv 文件进行排序

Java sort a csv file based on column date

需要根据日期列对 csv 文件进行排序。这是 masterRecords 数组列表的样子

GBEP-1-2-4,FRAG,PMTypeEthernet,NEND,TDTN,15-MIN,Dec 15 2014  - 07:15:00 AM MYT,+0,COMPL
GBEP-1-2-1,FRAG,PMTypeEthernet,NEND,TDTN,15-MIN,Dec 15 2014  - 07:00:00 AM MYT,+0,COMPL
GBEP-2-2-1,FRAG,PMTypeEthernet,NEND,TDTN,15-MIN,Dec 15 2014  - 07:30:00 AM MYT,+0,COMPL

我需要根据日期 07:15:00、07:30:00 等进行排序。我创建了一个代码来进行排序:

// Date is fixed on per 15min interval
ArrayList<String> sortDate = new ArrayList<String>();
    sortDate.add(":00:");
    sortDate.add(":15:");
    sortDate.add(":30:");
    sortDate.add(":45:");

    BufferedWriter bw = new BufferedWriter(new FileWriter(tempPath + filename));

    for (int k = 0; k < sortDate.size(); k++) {
        String date = sortDate.get(k);
        for (int j = 0; j < masterRecords.size(); j++) {
            String[] splitLine = masterRecords.get(j).split(",", -1);
            if (splitLine[10].contains(date)) {
                bw.write(masterRecords.get(j) + System.getProperty("line.separator").replaceAll(String.valueOf((char) 0x0D), ""));
                masterRecords.remove(j);
            }
        }
    }
bw.close();

从上面可以看出,它将遍历第一个数组 (sortDate) 并在第二个数组(即 masterRecord)上再次循环并将其写入新文件。它似乎在整理新文件时工作,但我注意到我的 masterRecord 有 10000 条记录,但在创建新文件后记录缩小到 5000,我假设它是如何从主列表中删除记录的。有人知道为什么吗?

在循环中删除项目是不安全的。 您必须在迭代器上迭代数组,例如:

List<String> names = ....
Iterator<String> i = names.iterator();
while (i.hasNext()) {
   String s = i.next(); // must be called before you can call i.remove()
   // Do something
   i.remove();
}

文档说:

The iterators returned by this class's iterator and listIterator methods are fail-fast: if the list is structurally modified at any time after the iterator is created, in any way except through the iterator's own remove or add methods, the iterator will throw a ConcurrentModificationException. Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future.

by Lautaro Cozzani 正确。

现在来点完全不同的东西

为了好玩,这是一种完全不同的方法。

我使用了两个库:

Apache Commons CSV

Commons CSV 库处理各种 CSV 风格的解析。它可以 return 文件中行的列表,每一行由它们的 CSVRecord 对象表示。您可以向该对象询问第一个字段、第二个字段等。

乔达时间

Joda-Time 负责解析日期时间字符串。

避免使用 3 个字母的时区代码

注意:Joda-Time 拒绝尝试解析三字母时区代码 MYT。有充分的理由:那些 3 或 4 个字母的代码只是约定俗成,既不标准化也不唯一。我下面的示例代码假设您的所有数据都使用 MYT。我的代码分配了正确的时区名称 xxx。我建议您启发创建输入数据的人以了解 proper time zone names and about ISO 8601 字符串格式。

Java 8

我的示例代码需要 Java 8,使用新的 Lambda 语法和 "streams"。

示例代码

本例进行双层排序。首先,行按小时(00、15、30、45)排序。在每个组中,行按日期时间值排序(按年、月、月日和日时排序)。

首先我们打开 .csv 文本文件,并将其内容解析为 CSVRecord 个对象。

String filePathString = "/Users/brainydeveloper/input.csv";
try {
    Reader in = new FileReader( filePathString ); // Get the input file.
    List<CSVRecord> recs = CSVFormat.DEFAULT.parse( in ).getRecords(); // Parse the input file.

接下来,我们将这些 CSVRecord 个对象分别包装在一个更智能的 class 中,该对象提取我们关心的两个值:首先是日期时间,其次是该日期时间的分钟数。请进一步查看 class CsvRecWithDateTimeAndMinute.

的简单代码
    List<CsvRecWithDateTimeAndMinute> smartRecs = new ArrayList<>( recs.size() ); // Collect transformed data.
    for ( CSVRecord rec : recs ) { // For each CSV record…
        CsvRecWithDateTimeAndMinute smartRec = new CsvRecWithDateTimeAndMinute( rec ); // …transform CSV rec into one of our objects with DateTime and minute-of-hour.
        smartRecs.add( smartRec );
    }

接下来,我们获取我们更智能的包装对象列表,并将该列表分成多个列表。每个新列表都包含特定时刻(00、15、30 和 45)的 CSV 行数据。我们将这些存储在地图中。

如果我们的输入数据只出现这四个值,则生成的映射将只有四个键。实际上,您可以通过查找四个以上的密钥来进行健全性检查。额外的键意味着要么在解析时出现严重错误,要么某些数据具有意外的分钟值。

每个键(这些数字的整数)都指向我们的智能包装器对象列表。这是一些奇特的新 Lambda 语法。

    Map<Integer , List<CsvRecWithDateTimeAndMinute>> byMinuteOfHour = smartRecs.stream().collect( Collectors.groupingBy( CsvRecWithDateTimeAndMinute::getMinuteOfHour ) );

地图没有给我们我们的子列表,我们的键(小时整数)排序​​。我们可能会在获得 00 组之前取回 15 组。所以提取键,并对它们进行排序。

    // Access the map by the minuteOfHour value in order. We want ":00:" first, then ":15", then ":30:", and ":45:" last.
    List<Integer> minutes = new ArrayList<Integer>( byMinuteOfHour.keySet() ); // Fetch the keys of the map.
    Collections.sort( minutes ); // Sort that List of keys.

按照该有序键列表,向地图询问每个键的列表。需要对该数据列表进行排序以获得我们的二级排序(按日期时间)。

    List<CSVRecord> outputList = new ArrayList<>( recs.size() ); // Make an empty List in which to put our CSVRecords in double-sorted order.
    for ( Integer minute : minutes ) {
        List<CsvRecWithDateTimeAndMinute> list = byMinuteOfHour.get( minute );
        // Secondary sort. For each group of records with ":00:" (for example), sort them by their full date-time value.
        // Sort the List by defining an anonymous Comparator using new Lambda syntax in Java 8.
        Collections.sort( list , ( CsvRecWithDateTimeAndMinute r1 , CsvRecWithDateTimeAndMinute r2 ) -> {
            return r1.getDateTime().compareTo( r2.getDateTime() );
        } );
        for ( CsvRecWithDateTimeAndMinute smartRec : list ) {
            outputList.add( smartRec.getCSVRecord() );
        }
    }

我们完成了数据处理。现在是时候导出回 CSV 格式的文本文件了。

    // Now we have complete List of CSVRecord objects in double-sorted order (first by minute-of-hour, then by date-time).
    // Now let's dump those back to a text file in CSV format.
    try ( PrintWriter out = new PrintWriter( new BufferedWriter( new FileWriter( "/Users/brainydeveloper/output.csv" ) ) ) ) {
        final CSVPrinter printer = CSVFormat.DEFAULT.print( out );
        printer.printRecords( outputList );
    }

} catch ( FileNotFoundException ex ) {
    System.out.println( "ERROR - Exception needs to be handled." );
} catch ( IOException ex ) {
    System.out.println( "ERROR - Exception needs to be handled." );
}

上面的代码一次将整个 CSV 数据集加载到内存中。如果希望节省内存,请使用 parse 方法而不是 getRecords 方法。至少那是医生似乎在说的。我还没有尝试过,因为到目前为止我的用例都很容易放入内存中。

这是包装每个 CSVRecord 对象的智能 class:

package com.example.jodatimeexperiment;

import org.apache.commons.csv.CSVRecord;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

/**
 *
 * @author Basil Bourque
 */
public class CsvRecWithDateTimeAndMinute
{

    // Statics
    static public final DateTimeFormatter FORMATTER = DateTimeFormat.forPattern( "MMM dd yyyy'  - 'hh:mm:ss aa 'MYT'" ).withZone( DateTimeZone.forID( "Asia/Kuala_Lumpur" ) );

    // Member vars.
    private final CSVRecord rec;
    private final DateTime dateTime;
    private final Integer minuteOfHour;

    public CsvRecWithDateTimeAndMinute( CSVRecord recordArg )
    {
        this.rec = recordArg;
        // Parse record to extract DateTime.
        // Expect value such as: Dec 15 2014  - 07:15:00 AM MYT
        String input = this.rec.get( 7 - 1 );  // Index (zero-based counting). So field # 7 = index # 6.
        this.dateTime = CsvRecWithDateTimeAndMinute.FORMATTER.parseDateTime( input );
        // From DateTime extract minute of hour
        this.minuteOfHour = this.dateTime.getMinuteOfHour();
    }

    public DateTime getDateTime()
    {
        return this.dateTime;
    }

    public Integer getMinuteOfHour()
    {
        return this.minuteOfHour;
    }

    public CSVRecord getCSVRecord()
    {
        return this.rec;
    }

    @Override
    public String toString()
    {
        return "CsvRecWithDateTimeAndMinute{ " + " minuteOfHour=" + minuteOfHour + " | dateTime=" + dateTime + " | rec=" + rec + " }";
    }

}

有了这个输入…

GBEP-1-2-4,FRAG,PMTypeEthernet,NEND,TDTN,15 分钟,2014 年 12 月 15 日 - 07:15:00 AM MYT,+0,COMPL GBEP-1-2-1,FRAG,PMTypeEthernet,NEND,TDTN,15 分钟,2014 年 12 月 15 日 - 07:00:00 AM MYT,+0,COMPL GBEP-2-2-1,FRAG,PMTypeEthernet,NEND,TDTN,15 分钟,2014 年 12 月 15 日 - 07:30:00 AM MYT,+0,COMPL GBEP-1-2-4,FRAG,PMTypeEthernet,NEND,TDTN,15 分钟,2014 年 12 月 14 日 - 07:15:00 AM MYT,+0,COMPL GBEP-1-2-1,FRAG,PMTypeEthernet,NEND,TDTN,15 分钟,2014 年 12 月 14 日 - 07:00:00 AM MYT,+0,COMPL GBEP-2-2-1,FRAG,PMTypeEthernet,NEND,TDTN,15 分钟,2014 年 12 月 14 日 - 07:30:00 AM MYT,+0,COMPL GBEP-1-2-4,FRAG,PMTypeEthernet,NEND,TDTN,15 分钟,2014 年 1 月 22 日 - 07:15:00 AM MYT,+0,COMPL GBEP-1-2-1,FRAG,PMTypeEthernet,NEND,TDTN,15 分钟,2014 年 1 月 22 日 - 07:00:00 AM MYT,+0,COMPL GBEP-2-2-1,FRAG,PMTypeEthernet,NEND,TDTN,15 分钟,2014 年 1 月 22 日 - 07:30:00 AM MYT,+0,COMPL

…你会得到这个输出…

GBEP-1-2-1,FRAG,PMTypeEthernet,NEND,TDTN,15 分钟,2014 年 1 月 22 日 - 07:00:00 AM MYT,+0,COMPL GBEP-1-2-1,FRAG,PMTypeEthernet,NEND,TDTN,15 分钟,2014 年 12 月 14 日 - 07:00:00 AM MYT,+0,COMPL GBEP-1-2-1,FRAG,PMTypeEthernet,NEND,TDTN,15 分钟,2014 年 12 月 15 日 - 07:00:00 AM MYT,+0,COMPL GBEP-1-2-4,FRAG,PMTypeEthernet,NEND,TDTN,15 分钟,2014 年 1 月 22 日 - 07:15:00 AM MYT,+0,COMPL GBEP-1-2-4,FRAG,PMTypeEthernet,NEND,TDTN,15 分钟,2014 年 12 月 14 日 - 07:15:00 AM MYT,+0,COMPL GBEP-1-2-4,FRAG,PMTypeEthernet,NEND,TDTN,15 分钟,2014 年 12 月 15 日 - 07:15:00 AM MYT,+0,COMPL GBEP-2-2-1,FRAG,PMTypeEthernet,NEND,TDTN,15 分钟,2014 年 1 月 22 日 - 07:30:00 AM MYT,+0,COMPL GBEP-2-2-1,FRAG,PMTypeEthernet,NEND,TDTN,15 分钟,2014 年 12 月 14 日 - 07:30:00 AM MYT,+0,COMPL GBEP-2-2-1,FRAG,PMTypeEthernet,NEND,TDTN,15 分钟,2014 年 12 月 15 日 - 07:30:00 AM MYT,+0,COMPL