💄 Format times on metrics page better
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Wed, 31 Jul 2019 14:37:56 +0000 (16:37 +0200)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Wed, 31 Jul 2019 14:37:56 +0000 (16:37 +0200)
src/main/kotlin/net/pterodactylus/sone/template/DurationFormatFilter.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/sone/web/WebInterfaceModule.kt
src/main/resources/templates/metrics.html
src/test/kotlin/net/pterodactylus/sone/template/DurationFormatFilterTest.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/sone/web/WebInterfaceModuleTest.kt

diff --git a/src/main/kotlin/net/pterodactylus/sone/template/DurationFormatFilter.kt b/src/main/kotlin/net/pterodactylus/sone/template/DurationFormatFilter.kt
new file mode 100644 (file)
index 0000000..559c9da
--- /dev/null
@@ -0,0 +1,67 @@
+package net.pterodactylus.sone.template
+
+import net.pterodactylus.util.template.*
+import java.time.*
+
+class DurationFormatFilter : Filter {
+
+       override fun format(templateContext: TemplateContext?, data: Any?, parameters: Map<String, Any?>?): Any? {
+               if (data is Number) {
+                       val scale = parameters?.get("scale")
+                       val duration = when (scale) {
+                               "ms" -> Duration.ofSeconds(data.toLong() / 1_000, (data.toDouble() * 1_000_000 % 1_000_000_000).toLong())
+                               "μs" -> Duration.ofSeconds(data.toLong() / 1_000_000, (data.toDouble() * 1_000 % 1_000_000_000).toLong())
+                               "ns" -> Duration.ofSeconds(data.toLong() / 1_000_000_000, data.toLong() % 1_000_000_000)
+                               else -> Duration.ofSeconds(data.toLong(), (data.toDouble() * 1_000_000_000 % 1_000_000_000).toLong())
+                       }
+                       return FixedDuration.values()
+                                       .map { it to it.number(duration) }
+                                       .firstOrNull { it.second >= 1 }
+                                       ?.let { "${"%.1f".format(it.second)}${it.first.symbol}" }
+                                       ?: "0s"
+               }
+               return data
+       }
+
+}
+
+@Suppress("unused")
+private enum class FixedDuration {
+
+       WEEKS {
+               override fun number(duration: Duration) = DAYS.number(duration) / 7.0
+               override val symbol = "w"
+       },
+       DAYS {
+               override fun number(duration: Duration) = HOURS.number(duration) / 24
+               override val symbol = "d"
+       },
+       HOURS {
+               override fun number(duration: Duration) = MINUTES.number(duration) / 60
+               override val symbol = "h"
+       },
+       MINUTES {
+               override fun number(duration: Duration) = SECONDS.number(duration) / 60
+               override val symbol = "m"
+       },
+       SECONDS {
+               override fun number(duration: Duration) = duration.seconds + duration.nano / 1_000_000_000.0
+               override val symbol = "s"
+       },
+       MILLIS {
+               override fun number(duration: Duration) = duration.nano / 1_000_000.0
+               override val symbol = "ms"
+       },
+       MICROS {
+               override fun number(duration: Duration) = duration.nano / 1_1000.0
+               override val symbol = "μs"
+       },
+       NANOS {
+               override fun number(duration: Duration) = duration.nano.toDouble()
+               override val symbol = "ns"
+       };
+
+       abstract fun number(duration: Duration): Double
+       abstract val symbol: String
+
+}
index dd5e9f6..283b1ea 100644 (file)
@@ -65,6 +65,7 @@ class WebInterfaceModule : AbstractModule() {
                                addFilter("reparse", ReparseFilter())
                                addFilter("unknown", unknownDateFilter)
                                addFilter("format", FormatFilter())
+                               addFilter("duration", DurationFormatFilter())
                                addFilter("sort", CollectionSortFilter())
                                addFilter("image-link", imageLinkFilter)
                                addFilter("replyGroup", ReplyGroupFilter())
index 2880e78..beaded8 100644 (file)
                        <tr>
                                <td><%= Page.Metrics.SoneInsertDuration.Title|l10n|html></td>
                                <td class="numeric"><% soneInsertDurationCount|html></td>
-                               <td class="numeric"><% soneInsertDurationMin|html>μs</td>
-                               <td class="numeric"><% soneInsertDurationMax|html>μs</td>
-                               <td class="numeric"><% soneInsertDurationMean|format format=='%.0f'|html>μs</td>
-                               <td class="numeric"><% soneInsertDurationMedian|format format=='%.0f'|html>μs</td>
-                               <td class="numeric"><% soneInsertDurationPercentile75|format format=='%.0f'|html>μs</td>
-                               <td class="numeric"><% soneInsertDurationPercentile95|format format=='%.0f'|html>μs</td>
-                               <td class="numeric"><% soneInsertDurationPercentile98|format format=='%.0f'|html>μs</td>
-                               <td class="numeric"><% soneInsertDurationPercentile99|format format=='%.0f'|html>μs</td>
-                               <td class="numeric"><% soneInsertDurationPercentile999|format format=='%.0f'|html>μs</td>
+                               <td class="numeric"><% soneInsertDurationMin|duration scale=="μs"|html></td>
+                               <td class="numeric"><% soneInsertDurationMax|duration scale=="μs"|html></td>
+                               <td class="numeric"><% soneInsertDurationMean|duration scale=="μs"|html></td>
+                               <td class="numeric"><% soneInsertDurationMedian|duration scale=="μs"|html></td>
+                               <td class="numeric"><% soneInsertDurationPercentile75|duration scale=="μs"|html></td>
+                               <td class="numeric"><% soneInsertDurationPercentile95|duration scale=="μs"|html></td>
+                               <td class="numeric"><% soneInsertDurationPercentile98|duration scale=="μs"|html></td>
+                               <td class="numeric"><% soneInsertDurationPercentile99|duration scale=="μs"|html></td>
+                               <td class="numeric"><% soneInsertDurationPercentile999|duration scale=="μs"|html></td>
                        </tr>
                        <tr>
                                <td><%= Page.Metrics.SoneParsingDuration.Title|l10n|html></td>
                                <td class="numeric"><% soneParsingDurationCount|html></td>
-                               <td class="numeric"><% soneParsingDurationMin|html>μs</td>
-                               <td class="numeric"><% soneParsingDurationMax|html>μs</td>
-                               <td class="numeric"><% soneParsingDurationMean|format format=='%.0f'|html>μs</td>
-                               <td class="numeric"><% soneParsingDurationMedian|format format=='%.0f'|html>μs</td>
-                               <td class="numeric"><% soneParsingDurationPercentile75|format format=='%.0f'|html>μs</td>
-                               <td class="numeric"><% soneParsingDurationPercentile95|format format=='%.0f'|html>μs</td>
-                               <td class="numeric"><% soneParsingDurationPercentile98|format format=='%.0f'|html>μs</td>
-                               <td class="numeric"><% soneParsingDurationPercentile99|format format=='%.0f'|html>μs</td>
-                               <td class="numeric"><% soneParsingDurationPercentile999|format format=='%.0f'|html>μs</td>
+                               <td class="numeric"><% soneParsingDurationMin|duration scale=="μs"|html></td>
+                               <td class="numeric"><% soneParsingDurationMax|duration scale=="μs"|html></td>
+                               <td class="numeric"><% soneParsingDurationMean|duration scale=="μs"|html></td>
+                               <td class="numeric"><% soneParsingDurationMedian|duration scale=="μs"|html></td>
+                               <td class="numeric"><% soneParsingDurationPercentile75|duration scale=="μs"|html></td>
+                               <td class="numeric"><% soneParsingDurationPercentile95|duration scale=="μs"|html></td>
+                               <td class="numeric"><% soneParsingDurationPercentile98|duration scale=="μs"|html></td>
+                               <td class="numeric"><% soneParsingDurationPercentile99|duration scale=="μs"|html></td>
+                               <td class="numeric"><% soneParsingDurationPercentile999|duration scale=="μs"|html></td>
                        </tr>
                </tbody>
        </table>
diff --git a/src/test/kotlin/net/pterodactylus/sone/template/DurationFormatFilterTest.kt b/src/test/kotlin/net/pterodactylus/sone/template/DurationFormatFilterTest.kt
new file mode 100644 (file)
index 0000000..9e378a8
--- /dev/null
@@ -0,0 +1,88 @@
+/**
+ * Sone - DurationFormatFilterTest.kt - Copyright © 2019 David ‘Bombe’ Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.template
+
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import kotlin.test.*
+
+class DurationFormatFilterTest {
+
+       private val filter = DurationFormatFilter()
+
+       @Test
+       fun `random object is returned as it is`() {
+               val randomObject = Any()
+               assertThat(filter.format(null, randomObject, emptyMap()), sameInstance(randomObject))
+       }
+
+       @Test
+       fun `integer 0 is rendered as “0s”`() {
+               verifyDuration(0, "0s")
+       }
+
+       @Test
+       fun `long 0 is rendered as “0s”`() {
+               verifyDuration(0L, "0s")
+       }
+
+       @Test
+       fun `12 is rendered as “12_0s”`() {
+               verifyDuration(12, "12.0s")
+       }
+
+       @Test
+       fun `123 is rendered as “2_1m”`() {
+               verifyDuration(123, "2.1m")
+       }
+
+       @Test
+       fun `12345 is rendered as “3_4h”`() {
+               verifyDuration(12345, "3.4h")
+       }
+
+       @Test
+       fun `123456 is rendered as “1_4d”`() {
+               verifyDuration(123456, "1.4d")
+       }
+
+       @Test
+       fun `1234567 is rendered as “2_0w”`() {
+               verifyDuration(1234567, "2.0w")
+       }
+
+       @Test
+       fun `123456789 with scale ms is rendered as “1_4d”`() {
+               verifyDuration(123456789, "1.4d", "ms")
+       }
+
+       @Test
+       fun `123456789 with scale μs is rendered as “2_1m”`() {
+               verifyDuration(123456789, "2.1m", "μs")
+       }
+
+       @Test
+       fun `123456789 with scale ns is rendered as “123_5ms”`() {
+               verifyDuration(123456789, "123.5ms", "ns")
+       }
+
+       private fun verifyDuration(value: Any, expectedRendering: String, scale: String? = null) {
+               assertThat(filter.format(null, value, scale?.let { mapOf("scale" to scale) } ?: emptyMap()), equalTo<Any>(expectedRendering))
+       }
+
+}
index 9137da3..ec09b20 100644 (file)
@@ -20,6 +20,7 @@ import net.pterodactylus.util.web.*
 import org.hamcrest.MatcherAssert.*
 import org.hamcrest.Matchers.*
 import org.junit.*
+import kotlin.test.Test
 
 class WebInterfaceModuleTest {
 
@@ -200,6 +201,11 @@ class WebInterfaceModuleTest {
        }
 
        @Test
+       fun `template context contains duration format filter`() {
+               verifyFilter<DurationFormatFilter>("duration")
+       }
+
+       @Test
        fun `template context contains collection sort filter`() {
                verifyFilter<CollectionSortFilter>("sort")
        }