javadurationlocaltimedatetime-parsingdatetimeparseexception

Text could not be parsed, unparsed text found


I am unable to figure out why I am getting the DateTimeParseException error when the text I am passing through fits the format. Below is the code that causes the issue:

LocalTime lt = LocalTime.parse(songTime, 
DateTimeFormatter.ofPattern("k:m:s"));

Here is the weird thing. Whenever I query the user for a time (let's use 00:02:30 as an example), it runs exactly as I want. But when I use my method (which extracts the time from a text file) it gives the error:

Exception in thread "main" java.time.format.DateTimeParseException: Text '00:02:30​' could not be parsed, unparsed text found at index 8

The first thing I assumed, was that it might of been bringing in a extra whitespace or something along the lines of that. So to check this, I printed 3 lines on each side of the variable and it printed this:

---00:02:30---

As you can see above, there were no white spaces. If I hardcode 00:02:30 then it works perfectly as well. I then ran into another thing that bugged me. My text file looks like this:

00:00:00 First
00:02:30 Second

The first time passes perfectly but anyone after that causes the error. They both have the exact same format with no whitespace on either side so I fail to see the problem. I checked every single forum post on the issue and majority of them were the individuals using the wrong formats, wrong strings etc. I am not sure that is the case here as it works perfectly when I hardcode it or query the user for input.

Below are the meanings of each option I chose in the Formatter (from the documentation):

k       clock-hour-of-am-pm (1-24)
m       minute-of-hour 
s       second-of-minute

Here is the method that reads the file:

public static ArrayList<Song> scanInSongs () {

ArrayList<Song> songArray = new ArrayList<Song>();

try { 

    BufferedReader reader = new BufferedReader(new FileReader("Description.txt"));
    String line;

    while ((line = reader.readLine()) != null) {
       String key = line.substring(0, line.indexOf(' '));
       System.out.println("Fetched timestamp: "+ key);
       String value = line.substring(line.indexOf(' ') + 1);
       System.out.println("Fetched name: "+ value);

       Song song = new Song(value, "", key);
       songArray.add(song);
    }
} catch (IOException e) {
    System.out.println("File not found, exception: "+ e);
} 

return songArray;

}

The Song Class:

public class Song {
    private String duration = "";
    private String name = "";
    private String timestampFromVideo = "";

    public Song(String name, String timestampFromVideo, String duration) {
        if (name == "") {
            this.name = "";   
        } else {
            this.name = name;
        }
        this.duration = duration;

        this.timestampFromVideo = timestampFromVideo;
    }

    public String getName() {
        return this.name;
    }

    public String getDuration() {
        return this.duration;
    }

    public String getTimestampFromVideo() {
        return this.timestampFromVideo;
    }

    public void setDuration(String duration) {
        this.duration = duration;
    }

}

The main:

public static void main(String[] args) {

    ArrayList<Song> songArray = scanInSongs();

    String songTime = songArray.get(0).getDuration();

    LocalTime lt = LocalTime.parse(songTime, 
    DateTimeFormatter.ofPattern("k:m:s"));       
}

Lastly as stated before is the file:

00:00:00 First
00:02:30 Second

Thank you in advance for the help everyone!


Solution

  • A non-printing character in your file

    There is a non-printing character in you string (so not white-space since strip() did not help). Since the string has been read from the file, that character must be in the file.

    How to check:

        key.chars().forEach(System.out::println);
    

    If key is just "00:02:30", this would print

    48
    48
    58
    48
    50
    58
    51
    48
    

    I bet that you got more output than this. If for example you’ve got:

    48
    48
    58
    48
    50
    58
    51
    48
    8203
    

    Here we can see that there’s a character with unicode value 8203 (decimal) at the end of the string. It’s a zero-width space, which explains why we could not see it when you printed the string. LocalTime.parse() could see it, which explains the error message that you got.

    Interesting (and perhaps disappointing) a zero-width space does not count as white space for the strip method suggested by anish sharma in his answer, which is why that answer did not solve the problem.

    The good solution is to remove the character from your file.

    Another suggestion for improvement: java.time.Duration

    I fully agree with the answer by Arvind Kumar Avinash: you should use the Duration class for a duration (also LocalTime will no longer work should you one day encounter a duration longer than 24 hours). And you should take the use of Duration a step further and in your model class keep the duration as a Duration, not as a string. Just like you keep numbers in int variables and not in strings (I very much hope).

    public class Song {
        private Duration duration;
        private String name;
        // …
    

    Since your instance variables are always initialized from the constructor, do not give them default values in the declaration, that will just confuse. You may write a convenience constructor that parses the string into a Duration, for example:

    /** @throws DateTimeParseException if durationString is not in format hh:mm:ss */
    public Song(String name, String timestampFromVideo, String durationString) {
        this.name = "";   
        String iso = durationString.replaceFirst(
                "^(\\d{2}):(\\d{2}):(\\d{2})$", "PT$1H$2M$3S");
        duration = Duration.parse(iso);
    

    Your if statement testing whether name is empty was both wrong and redundant, so I left it out. You cannot compare strings using ==. To test whether a string is empty use name.isEmpty() (requires that the string is non-null, which it is when you have read it from a file). Since you were assigning the empty string to name anyway, I found it simpler to omit the check.

    My way of converting the duration string to the ISO 8601 format required by Duration.parse() differs from the answer by Arvind Kumar Avinash. If his way is easier to understand, by all means use it, it’s fine.

    Bonus: If you want the conversion to ISO 8601 to leave out any characters after the seconds, including a zero-width space, use this variant:

        String iso = durationString.replaceFirst(
                "^(\\d{2}):(\\d{2}):(\\d{2}).*$", "PT$1H$2M$3S");
    

    I inserted .* before the $ denoting the end of the string. This matches any characters that might be there and causes them not to get into the ISO 8601 string. So now parsing also works when there are zero-width spaces in the file.

    Furthermore, not relevant if you use Duration, Arvind is also correct that if you wanted to parse into a LocalTime, you would not need a DateTimeFormatter since 00:02:30 is in ISO 8601 format for a time of day and LocalTime parses this format without a formatter being specified. Finally Joachim Isaksson is correct in the comment that pattern letter k is incorrect for hour of day when it can be 0. k is for clock-hour-of-day (1-24). For hour of day (00–23) you would have needed HH.

    Links