7

I'm working on a Android application and I need to parse a duration string of this form "00:00:00.000" (Ex: "00:00:38.47" or "00:03:27.11").

My final goal is to convert this string into a double of total seconds with milliseconds to get a progression percent.

I already looked into SimpleDateFormatter, but I'm having some issue understanding how it works or how I'm supposed to use it.

I've tried this:

val timeString = "00:01:08.83"
val df = SimpleDateFormat("HH:mm:ss.SS")
df.parse(timeString)?.seconds!!.toFloat() // Returns 8.00, I want to have 68.83 for this example string

So is there a simple/efficient way of doing this ?

NhgrtPlayer
  • 159
  • 3
  • 6
  • As an aside consider throwing away the long outmoded and notoriously troublesome `SimpleDateFormat` and friends, and adding [ThreeTenABP](https://github.com/JakeWharton/ThreeTenABP) to your Android project in order to use `java.time`, the modern Java date and time API. It is so much nicer to work with. – Ole V.V. Nov 03 '19 at 12:21
  • Possible duplicate of [Java: How to convert a string (HH:MM:SS) to a duration?](https://stackoverflow.com/questions/8257641/java-how-to-convert-a-string-hhmmss-to-a-duration) And/or of [Java 8 Time API: how to parse string of format “mm:ss” to Duration?](https://stackoverflow.com/questions/24642495/java-8-time-api-how-to-parse-string-of-format-mmss-to-duration) There are more questions like it, please search. – Ole V.V. Nov 03 '19 at 12:25
  • I've searched before asking my question, but I couldn't find a definitive answer to my (specific ?) question, current accepted answer isn't in your proposed duplicates – NhgrtPlayer Nov 03 '19 at 12:53

3 Answers3

5
  val time = LocalTime.parse("00:01:08.83", DateTimeFormatter.ofPattern("HH:mm:ss.SS"))

  println(time.toSecondOfDay())
  println(time.toNanoOfDay())
  println(time.toNanoOfDay().toFloat() / 1_000_000_000)

Output:

68
68830000000
68.83
Lachezar Balev
  • 11,498
  • 9
  • 49
  • 72
  • Looks great, but do we have a built-in constant for this 1 billion ? Random constant value isn't good for my health :D – NhgrtPlayer Nov 02 '19 at 13:25
  • 1
    There is one, but it has package visibility in LocalTime... The good news is that the number of nanoseconds within a second will not change soon :-) – Lachezar Balev Nov 02 '19 at 18:31
  • 4
    Note that this works only if hours component is less than 24. – Ilya Nov 02 '19 at 22:18
  • A `LocalTime` is a time of day. For a duration you want a `Duration` object. Using one for the other is not correct is very likely to confuse those reading your code. – Ole V.V. Nov 03 '19 at 12:30
  • That's a good point, but looking at your answer, using a duration looks way more annoying that using this. It's only one of code that can be easily commented – NhgrtPlayer Nov 03 '19 at 15:41
  • Using the `float` type may [introduce mathematical inaccuracies](https://en.wikipedia.org/wiki/Floating-point_arithmetic#Accuracy_problems). Better to use the class built for the purpose of representing a span of time unattached to the timeline, [`Duration`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/Duration.html), as seen in the [Answer by Ole V.V.](https://stackoverflow.com/a/58680750/642706). – Basil Bourque Nov 03 '19 at 16:26
4

java.time.Duration

As shown in the linked questions there are several ways to do this. I am afraid that there isn’t one objectively best way.

You should most probably use a Duration from java.time, the modern Java date and time API, for your duration.

Unfortunately there isn’t a way to parse your string directly into a Duration. My preference is for modifying your string into ISO 8601 format, the format that the Duration class knows how to parse. I trust you to translate my Java code into even more beautiful Kotlin code.

    String timeString = "00:01:08.83";
    String isoDurationString = timeString
            .replaceFirst("(\\d+):(\\d{2}):(\\d{2}(?:\\.\\d+)?)", "PT$1H$2M$3S");
    Duration dur = Duration.parse(isoDurationString);
    System.out.println(dur);

Output from this snippet is:

PT1M8.83S

The regular expression is powerful but hard to read. The round brackets denote groups that I want to keep in the modified string; I refer to them as $1 etc. in the replacement string. (?:\\.\\d+) is a non-capturing group, one that I don’t need to use in the replacement. The ? after the non-capturing group says that it needs not be there (so the expression matches just 00:01:08 as well).

For a percentage there are some options again. Duration objects can be directly multiplied by 100 and since Java 9 divided by each other. Assuming that you are not yet on Java 9, I would probably make the calculation based on milliseconds or nanoseconds (rather than seconds with a fraction). For example:

    long totalMilliseconds = dur.toMillis();
    System.out.println(totalMilliseconds);

68830

However to answer your question here’s how I would convert to seconds in a float:

    float totalSeconds = ((float) dur.toNanos()) / TimeUnit.SECONDS.toNanos(1);
    System.out.println(totalSeconds);

68.83

Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
Ole V.V.
  • 81,772
  • 15
  • 137
  • 161
0

The method getSeconds() that you use returns only the seconds of the parsed time and also it is deprecated.
If you can't use LocalTime.parse() in your Android app because it requires API level 26, then split the time string and parse it by multiplying each part with the appropriate factor:

val timeString = "00:01:08.83"
val factors = arrayOf(3600.0, 60.0, 1.0, 0.01)
var value = 0.0
timeString.replace(".", ":").split(":").forEachIndexed { i, s -> value += factors[i] * s.toDouble() }
println(value)

will print:

68.83

You could also create an extension function:

fun String.toSeconds(): Double {
    val factors = arrayOf(3600.0, 60.0, 1.0, 0.01)
    var value = 0.0
    this.replace(".", ":").split(":").forEachIndexed { i, s -> value += factors[i] * s.toDouble() }
    return value
}

and use it:

val timeString = "00:01:08.83"
val seconds = timeString.toSeconds()
println(seconds)
forpas
  • 160,666
  • 10
  • 38
  • 76