diff --git a/base-portable/src/commonMain/kotlin/jetbrains/datalore/base/dateFormat/Format.kt b/base-portable/src/commonMain/kotlin/jetbrains/datalore/base/dateFormat/DateTimeFormat.kt similarity index 97% rename from base-portable/src/commonMain/kotlin/jetbrains/datalore/base/dateFormat/Format.kt rename to base-portable/src/commonMain/kotlin/jetbrains/datalore/base/dateFormat/DateTimeFormat.kt index 84061a0298b..99bece694a6 100644 --- a/base-portable/src/commonMain/kotlin/jetbrains/datalore/base/dateFormat/Format.kt +++ b/base-portable/src/commonMain/kotlin/jetbrains/datalore/base/dateFormat/DateTimeFormat.kt @@ -10,7 +10,7 @@ import jetbrains.datalore.base.datetime.Date import jetbrains.datalore.base.datetime.DateTime import jetbrains.datalore.base.datetime.Time -class Format(private val spec: List) { +class DateTimeFormat(private val spec: List) { constructor(spec: String): this(parse(spec)) @@ -19,7 +19,7 @@ class Format(private val spec: List) { } class PatternSpecPart(str: String): SpecPart(str) { - val pattern: Pattern = Pattern.patternByString(str) ?: throw IllegalArgumentException("Wrong pattern: $str") + val pattern: Pattern = Pattern.patternByString(str) ?: throw IllegalArgumentException("Wrong date-time pattern: $str") override fun exec(dateTime: DateTime): String { return getValueForPattern(pattern, dateTime) diff --git a/base-portable/src/commonMain/kotlin/jetbrains/datalore/base/dateFormat/Pattern.kt b/base-portable/src/commonMain/kotlin/jetbrains/datalore/base/dateFormat/Pattern.kt index 2b80a9e2239..2edaab3b6e1 100644 --- a/base-portable/src/commonMain/kotlin/jetbrains/datalore/base/dateFormat/Pattern.kt +++ b/base-portable/src/commonMain/kotlin/jetbrains/datalore/base/dateFormat/Pattern.kt @@ -50,5 +50,7 @@ enum class Pattern(val string: String, val kind: Kind) { } fun patternByString(patternString: String) = values().find { it.string == patternString } + + fun isDateTimeFormat(patternString: String) = PATTERN_REGEX.containsMatchIn(patternString) } } \ No newline at end of file diff --git a/base-portable/src/commonMain/kotlin/jetbrains/datalore/base/numberFormat/NumberFormat.kt b/base-portable/src/commonMain/kotlin/jetbrains/datalore/base/numberFormat/NumberFormat.kt index cfb0b9e5cae..714031193a2 100644 --- a/base-portable/src/commonMain/kotlin/jetbrains/datalore/base/numberFormat/NumberFormat.kt +++ b/base-portable/src/commonMain/kotlin/jetbrains/datalore/base/numberFormat/NumberFormat.kt @@ -486,7 +486,7 @@ class NumberFormat(private val spec: Spec) { fun isValidPattern(spec: String) = NUMBER_REGEX.matches(spec) private fun parse(spec: String): Spec { - val matchResult = NUMBER_REGEX.find(spec) ?: throw IllegalArgumentException("Wrong pattern format") + val matchResult = NUMBER_REGEX.find(spec) ?: throw IllegalArgumentException("Wrong number format pattern: '$spec'") return Spec( fill = matchResult.groups[1]?.value ?: " ", diff --git a/base-portable/src/commonMain/kotlin/jetbrains/datalore/base/stringFormat/StringFormat.kt b/base-portable/src/commonMain/kotlin/jetbrains/datalore/base/stringFormat/StringFormat.kt index 5eb538eb412..c796dec3671 100644 --- a/base-portable/src/commonMain/kotlin/jetbrains/datalore/base/stringFormat/StringFormat.kt +++ b/base-portable/src/commonMain/kotlin/jetbrains/datalore/base/stringFormat/StringFormat.kt @@ -5,7 +5,12 @@ package jetbrains.datalore.base.stringFormat +import jetbrains.datalore.base.dateFormat.DateTimeFormat +import jetbrains.datalore.base.dateFormat.Pattern.Companion.isDateTimeFormat +import jetbrains.datalore.base.datetime.Instant +import jetbrains.datalore.base.datetime.tz.TimeZone import jetbrains.datalore.base.numberFormat.NumberFormat +import jetbrains.datalore.base.stringFormat.StringFormat.FormatType.* class StringFormat private constructor( private val pattern: String, @@ -13,55 +18,48 @@ class StringFormat private constructor( ) { enum class FormatType { NUMBER_FORMAT, + DATETIME_FORMAT, STRING_FORMAT } - private val myNumberFormatters: List + private val myFormatters: List<((Any) -> String)> init { - fun initNumberFormat(pattern: String): NumberFormat { - try { - return NumberFormat(pattern) - } catch (e: Exception) { - error("Wrong number pattern: $pattern") - } - } - - myNumberFormatters = when (formatType) { - FormatType.NUMBER_FORMAT -> listOf(initNumberFormat(pattern)) - FormatType.STRING_FORMAT -> { + myFormatters = when (formatType) { + NUMBER_FORMAT, DATETIME_FORMAT -> listOf(initFormatter(pattern, formatType)) + STRING_FORMAT -> { BRACES_REGEX.findAll(pattern) .map { it.groupValues[TEXT_IN_BRACES] } .map { format -> - if (format.isNotEmpty()) { - initNumberFormat(format) - } else { - null + val formatType = detectFormatType(format) + require(formatType == NUMBER_FORMAT || formatType == DATETIME_FORMAT) { + error("Can't detect type of pattern '$format' used in string pattern '$pattern'") } + initFormatter(format, formatType) } .toList() } } } - val argsNumber = myNumberFormatters.size + val argsNumber = myFormatters.size fun format(value: Any): String = format(listOf(value)) fun format(values: List): String { if (argsNumber != values.size) { - error("Can't format values $values with pattern \"$pattern\"). Wrong number of arguments: expected $argsNumber instead of ${values.size}") + error("Can't format values $values with pattern '$pattern'). Wrong number of arguments: expected $argsNumber instead of ${values.size}") } return when (formatType) { - FormatType.NUMBER_FORMAT -> { - require(myNumberFormatters.size == 1) - formatValue(values.single(), myNumberFormatters.single()) + NUMBER_FORMAT, DATETIME_FORMAT -> { + require(myFormatters.size == 1) + formatValue(values.single(), myFormatters.single()) } - FormatType.STRING_FORMAT -> { + STRING_FORMAT -> { var index = 0 BRACES_REGEX.replace(pattern) { val originalValue = values[index] - val formatter = myNumberFormatters[index++] + val formatter = myFormatters[index++] formatValue(originalValue, formatter) } .replace("{{", "{") @@ -70,12 +68,43 @@ class StringFormat private constructor( } } - private fun formatValue(value: Any, numberFormatter: NumberFormat?): String { - return when { - numberFormatter == null -> value.toString() - value is Number -> numberFormatter.apply(value) - value is String -> value.toFloatOrNull()?.let(numberFormatter::apply) ?: value - else -> error("Failed to format value with type ${value::class.simpleName}. Supported types are Number and String.") + private fun initFormatter(formatPattern: String, formatType: FormatType): ((Any) -> String) { + if (formatPattern.isEmpty()) { + return Any::toString + } + when (formatType) { + NUMBER_FORMAT -> { + val numberFormatter = NumberFormat(formatPattern) + return { value: Any -> + when (value) { + is Number -> numberFormatter.apply(value) + is String -> value.toFloatOrNull()?.let(numberFormatter::apply) ?: value + else -> error("Failed to format value with type ${value::class.simpleName}. Supported types are Number and String.") + } + } + } + DATETIME_FORMAT -> { + val dateTimeFormatter = DateTimeFormat(formatPattern) + return { value: Any -> + require(value is Number) { + error("Value '$value' to be formatted as DateTime expected to be a Number, but was ${value::class.simpleName}") + } + value.toLong() + .let(::Instant) + .let(TimeZone.UTC::toDateTime) + .let(dateTimeFormatter::apply) + } + } + else -> { + error("Undefined format pattern $formatPattern") + } + } + } + + private fun formatValue(value: Any, formatter: ((Any) -> String)?): String { + return when (formatter) { + null -> value.toString() + else -> formatter(value) } } @@ -102,25 +131,27 @@ class StringFormat private constructor( fun forNArgs( pattern: String, - type: FormatType? = null, argCount: Int, formatFor: String? = null ): StringFormat { - return create(pattern, type, formatFor, argCount) + return create(pattern, STRING_FORMAT, formatFor, argCount) } - fun create( + private fun detectFormatType(pattern: String): FormatType { + return when { + NumberFormat.isValidPattern(pattern) -> NUMBER_FORMAT + isDateTimeFormat(pattern) -> DATETIME_FORMAT + else -> STRING_FORMAT + } + } + + internal fun create( pattern: String, type: FormatType? = null, formatFor: String? = null, expectedArgs: Int = -1 ): StringFormat { - val formatType = when { - type != null -> type - NumberFormat.isValidPattern(pattern) -> FormatType.NUMBER_FORMAT - else -> FormatType.STRING_FORMAT - } - + val formatType = type ?: detectFormatType(pattern) return StringFormat(pattern, formatType).also { if (expectedArgs > 0) { require(it.argsNumber == expectedArgs) { diff --git a/base-portable/src/commonTest/kotlin/jetbrains/datalore/base/dateFormat/FormatDateTest.kt b/base-portable/src/commonTest/kotlin/jetbrains/datalore/base/dateFormat/FormatDateTest.kt index 3461dfcf157..4affb69385b 100644 --- a/base-portable/src/commonTest/kotlin/jetbrains/datalore/base/dateFormat/FormatDateTest.kt +++ b/base-portable/src/commonTest/kotlin/jetbrains/datalore/base/dateFormat/FormatDateTest.kt @@ -15,7 +15,7 @@ class FormatDateTest { @Test fun onlyDate() { - val f = Format("%Y-%m-%dT%H:%M:%S") + val f = DateTimeFormat("%Y-%m-%dT%H:%M:%S") assertEquals("2019-08-06T::", f.apply(date)) } } \ No newline at end of file diff --git a/base-portable/src/commonTest/kotlin/jetbrains/datalore/base/dateFormat/FormatDateTimeTest.kt b/base-portable/src/commonTest/kotlin/jetbrains/datalore/base/dateFormat/FormatDateTimeTest.kt index b5153715a04..e2ddfe16dda 100644 --- a/base-portable/src/commonTest/kotlin/jetbrains/datalore/base/dateFormat/FormatDateTimeTest.kt +++ b/base-portable/src/commonTest/kotlin/jetbrains/datalore/base/dateFormat/FormatDateTimeTest.kt @@ -19,28 +19,28 @@ class FormatDateTimeTest { @Test fun datePatterns() { - assertEquals("Tue", Format("%a").apply(dateTime)) - assertEquals("Tuesday", Format("%A").apply(dateTime)) - assertEquals("Aug", Format("%b").apply(dateTime)) - assertEquals("August", Format("%B").apply(dateTime)) - assertEquals("06", Format("%d").apply(dateTime)) - assertEquals("6", Format("%e").apply(dateTime)) - assertEquals("218", Format("%j").apply(dateTime)) - assertEquals("08", Format("%m").apply(dateTime)) - assertEquals("2", Format("%w").apply(dateTime)) - assertEquals("19", Format("%y").apply(dateTime)) - assertEquals("2019", Format("%Y").apply(dateTime)) + assertEquals("Tue", DateTimeFormat("%a").apply(dateTime)) + assertEquals("Tuesday", DateTimeFormat("%A").apply(dateTime)) + assertEquals("Aug", DateTimeFormat("%b").apply(dateTime)) + assertEquals("August", DateTimeFormat("%B").apply(dateTime)) + assertEquals("06", DateTimeFormat("%d").apply(dateTime)) + assertEquals("6", DateTimeFormat("%e").apply(dateTime)) + assertEquals("218", DateTimeFormat("%j").apply(dateTime)) + assertEquals("08", DateTimeFormat("%m").apply(dateTime)) + assertEquals("2", DateTimeFormat("%w").apply(dateTime)) + assertEquals("19", DateTimeFormat("%y").apply(dateTime)) + assertEquals("2019", DateTimeFormat("%Y").apply(dateTime)) } @Test fun timePatterns() { - assertEquals("04", Format("%H").apply(dateTime)) - assertEquals("04", Format("%I").apply(dateTime)) - assertEquals("4", Format("%l").apply(dateTime)) - assertEquals("46", Format("%M").apply(dateTime)) - assertEquals("am", Format("%P").apply(dateTime)) - assertEquals("AM", Format("%p").apply(dateTime)) - assertEquals("35", Format("%S").apply(dateTime)) + assertEquals("04", DateTimeFormat("%H").apply(dateTime)) + assertEquals("04", DateTimeFormat("%I").apply(dateTime)) + assertEquals("4", DateTimeFormat("%l").apply(dateTime)) + assertEquals("46", DateTimeFormat("%M").apply(dateTime)) + assertEquals("am", DateTimeFormat("%P").apply(dateTime)) + assertEquals("AM", DateTimeFormat("%p").apply(dateTime)) + assertEquals("35", DateTimeFormat("%S").apply(dateTime)) } @Test @@ -48,27 +48,27 @@ class FormatDateTimeTest { val date = Date(6, Month.JANUARY, 2019) val time = Time(4, 3, 2) val dateTime = DateTime(date, time) - assertEquals("04", Format("%H").apply(dateTime)) - assertEquals("04", Format("%I").apply(dateTime)) - assertEquals("4", Format("%l").apply(dateTime)) - assertEquals("03", Format("%M").apply(dateTime)) - assertEquals("02", Format("%S").apply(dateTime)) + assertEquals("04", DateTimeFormat("%H").apply(dateTime)) + assertEquals("04", DateTimeFormat("%I").apply(dateTime)) + assertEquals("4", DateTimeFormat("%l").apply(dateTime)) + assertEquals("03", DateTimeFormat("%M").apply(dateTime)) + assertEquals("02", DateTimeFormat("%S").apply(dateTime)) - assertEquals("06", Format("%d").apply(dateTime)) - assertEquals("6", Format("%e").apply(dateTime)) - assertEquals("006", Format("%j").apply(dateTime)) - assertEquals("01", Format("%m").apply(dateTime)) + assertEquals("06", DateTimeFormat("%d").apply(dateTime)) + assertEquals("6", DateTimeFormat("%e").apply(dateTime)) + assertEquals("006", DateTimeFormat("%j").apply(dateTime)) + assertEquals("01", DateTimeFormat("%m").apply(dateTime)) } @Test fun isoFormat() { - val f = Format("%Y-%m-%dT%H:%M:%S") + val f = DateTimeFormat("%Y-%m-%dT%H:%M:%S") assertEquals("2019-08-06T04:46:35", f.apply(dateTime)) } @Test fun randomFormat() { - val f = Format("----!%%%YY%md%dT%H:%M:%S%%%") + val f = DateTimeFormat("----!%%%YY%md%dT%H:%M:%S%%%") assertEquals("----!%%2019Y08d06T04:46:35%%%", f.apply(dateTime)) } } \ No newline at end of file diff --git a/base-portable/src/commonTest/kotlin/jetbrains/datalore/base/dateFormat/FormatTimeTest.kt b/base-portable/src/commonTest/kotlin/jetbrains/datalore/base/dateFormat/FormatTimeTest.kt index 65edfeb3796..8a813bcfab7 100644 --- a/base-portable/src/commonTest/kotlin/jetbrains/datalore/base/dateFormat/FormatTimeTest.kt +++ b/base-portable/src/commonTest/kotlin/jetbrains/datalore/base/dateFormat/FormatTimeTest.kt @@ -14,7 +14,7 @@ class FormatTimeTest { @Test fun onlyTime() { - val f = Format("%Y-%m-%dT%H:%M:%S") + val f = DateTimeFormat("%Y-%m-%dT%H:%M:%S") assertEquals("--T04:46:35", f.apply(time)) } } \ No newline at end of file diff --git a/base-portable/src/jvmTest/kotlin/base/stringFormat/StringFormatTest.kt b/base-portable/src/jvmTest/kotlin/base/stringFormat/StringFormatTest.kt index aa9b4897f94..e6c9b166370 100644 --- a/base-portable/src/jvmTest/kotlin/base/stringFormat/StringFormatTest.kt +++ b/base-portable/src/jvmTest/kotlin/base/stringFormat/StringFormatTest.kt @@ -5,8 +5,12 @@ package jetbrains.datalore.base.stringFormat -import jetbrains.datalore.base.stringFormat.StringFormat.FormatType.NUMBER_FORMAT -import jetbrains.datalore.base.stringFormat.StringFormat.FormatType.STRING_FORMAT +import jetbrains.datalore.base.datetime.Date +import jetbrains.datalore.base.datetime.DateTime +import jetbrains.datalore.base.datetime.Month +import jetbrains.datalore.base.datetime.Time +import jetbrains.datalore.base.datetime.tz.TimeZone +import jetbrains.datalore.base.stringFormat.StringFormat.FormatType.* import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -20,6 +24,8 @@ class StringFormatTest { assertEquals(1, StringFormat.create("{.1f} test").argsNumber) assertEquals(2, StringFormat.create("{.1f} {}").argsNumber) assertEquals(3, StringFormat.create("{.1f} {.2f} {.3f}").argsNumber) + assertEquals(1, StringFormat.create("%d.%m.%y %H:%M", DATETIME_FORMAT).argsNumber) + assertEquals(2,StringFormat.create("at {%H:%M} on {%A}", STRING_FORMAT).argsNumber) } @Test @@ -93,7 +99,10 @@ class StringFormatTest { val exception = assertFailsWith(IllegalStateException::class) { StringFormat.create(formatPattern).format(valuesToFormat) } - assertEquals("Can't format values [1, 2] with pattern \"{.1f} x {.2f} x {.3f}\"). Wrong number of arguments: expected 3 instead of 2", exception.message) + assertEquals( + "Can't format values [1, 2] with pattern '{.1f} x {.2f} x {.3f}'). Wrong number of arguments: expected 3 instead of 2", + exception.message + ) } @Test @@ -118,11 +127,87 @@ class StringFormatTest { @Test fun `try to format static text as number format`() { + val exception = assertFailsWith(IllegalArgumentException::class) { + StringFormat.create("pattern", type = NUMBER_FORMAT).format("text") + } + assertEquals( + "Wrong number format pattern: 'pattern'", + exception.message + ) + } + + private val dateTimeToFormat = TimeZone.UTC.toInstant( + DateTime(Date(6, Month.AUGUST, 2019), Time(4, 46, 35)) + ).timeSinceEpoch + + @Test + fun `DateTime format`() { + assertEquals("August", StringFormat.create("%B").format(dateTimeToFormat)) + assertEquals("Tuesday", StringFormat.create("%A").format(dateTimeToFormat)) + assertEquals("2019", StringFormat.create("%Y").format(dateTimeToFormat)) + assertEquals("06.08.19", StringFormat.create("%d.%m.%y").format(dateTimeToFormat)) + assertEquals("06.08.19 04:46", StringFormat.create("%d.%m.%y %H:%M").format(dateTimeToFormat)) + } + + @Test + fun `string pattern with Number and DateTime`() { + val formatPattern = "{d}nd day of {%B}" + val valuesToFormat = listOf(2, dateTimeToFormat) + val formattedString = StringFormat.create(formatPattern, STRING_FORMAT).format(valuesToFormat) + assertEquals("2nd day of August", formattedString) + } + + @Test + fun `use DateTime format in the string pattern`() { + assertEquals( + "at 04:46 on Tuesday", + StringFormat.create( + pattern = "at {%H:%M} on {%A}", + type = STRING_FORMAT + ).format(listOf(dateTimeToFormat, dateTimeToFormat)) + ) + } + @Test + fun `DateTime format can be used to form the string without braces in its pattern`() { + assertEquals( + expected = "at 04:46 on Tuesday", + StringFormat.create( + pattern = "at %H:%M on %A", + type = DATETIME_FORMAT + ).format(dateTimeToFormat) + ) + } + + @Test + fun `Number pattern as DateTime format will return string with pattern`() { + assertEquals( + expected = ".1f", + StringFormat.create(".1f", type = DATETIME_FORMAT).format(dateTimeToFormat) + ) + } + + @Test + fun `try to format static text as DateTime format`() { val exception = assertFailsWith(IllegalStateException::class) { - StringFormat.create("text", type = NUMBER_FORMAT).format(emptyList()) + StringFormat.create("%d.%m.%y").format("01.01.2000") } assertEquals( - "Wrong number pattern: text", + "Value '01.01.2000' to be formatted as DateTime expected to be a Number, but was String", + exception.message + ) + } + + @Test + fun `try to use undefined pattern inside string pattern`() { + val formatPattern = "{.1f} x {PP}" + val valuesToFormat = listOf(1, 2) + + val exception = assertFailsWith(IllegalStateException::class) { + StringFormat.create(formatPattern).format(valuesToFormat) + } + + assertEquals( + "Can't detect type of pattern 'PP' used in string pattern '{.1f} x {PP}'", exception.message ) } diff --git a/docs/examples/jupyter-notebooks-dev/datetime_formatting.ipynb b/docs/examples/jupyter-notebooks-dev/datetime_formatting.ipynb new file mode 100644 index 00000000000..85fc173fdef --- /dev/null +++ b/docs/examples/jupyter-notebooks-dev/datetime_formatting.ipynb @@ -0,0 +1,397 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "innovative-intent", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "from datetime import datetime\n", + "from lets_plot import *\n", + "\n", + "LetsPlot.setup_html()" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "seasonal-immune", + "metadata": {}, + "outputs": [], + "source": [ + "economics_url = 'https://vincentarelbundock.github.io/Rdatasets/csv/ggplot2/economics.csv'\n", + "economics = pd.read_csv(economics_url)\n", + "economics['date'] = pd.to_datetime(economics['date'])\n", + "start = datetime(2000, 1, 1)\n", + "economics = economics.loc[economics['date'] >= start]" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "injured-awareness", + "metadata": {}, + "outputs": [], + "source": [ + "p = (ggplot(economics, aes('date', 'uempmed')) + \n", + " geom_line() + \n", + " ylab(\"unemployment rate\") +\n", + " ggsize(600, 400))" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "marine-folks", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Default\n", + "\n", + "p + scale_x_datetime()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "forbidden-honolulu", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Formatting to axis labels\n", + "\n", + "(p + \n", + " scale_x_datetime(format=\"%b %Y\") + \n", + " scale_y_continuous(format=\"{} %\")\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "approved-canon", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# The specified formatting also should be applied to 'breaks'\n", + "\n", + "breaks = pd.date_range(\n", + " pd.to_datetime(\"2001-01-01\"), \n", + " pd.to_datetime(\"2016-01-01\"), \n", + " freq='5AS'\n", + ").to_pydatetime()\n", + "\n", + "(p + \n", + " scale_x_datetime(format=\"%b %Y\", breaks=breaks) + \n", + " scale_y_continuous(format=\"{} %\")\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "informal-attendance", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Use date-time formatting in tooltip\n", + "\n", + "(ggplot(economics, aes('date', 'uempmed')) + \n", + " ylab(\"unemployment rate\") +\n", + " scale_x_datetime() +\n", + " scale_y_continuous() +\n", + " ggsize(600, 400) + \n", + " geom_line(tooltips=layer_tooltips()\n", + " .line('@uempmed % in @date')\n", + " .format('date', '%B %Y')\n", + " .color(\"black\")\n", + " .anchor(\"top_left\")\n", + " .min_width(170))\n", + ")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/Scale.kt b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/Scale.kt index 2192d14889b..4ed9b61d9e8 100644 --- a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/Scale.kt +++ b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/Scale.kt @@ -15,7 +15,7 @@ import jetbrains.datalore.plot.base.scale.ScaleBreaks * * * name - (axis/legend title) - * brakes (domain values) - ticks on axis, items/segments on legends + * breaks (domain values) - ticks on axis, items/segments on legends * labels - tick labels * * @param - type of target aesthetic diff --git a/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/ScaleProviderBuilder.kt b/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/ScaleProviderBuilder.kt index ab5b843e989..2ad9a46614b 100644 --- a/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/ScaleProviderBuilder.kt +++ b/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/ScaleProviderBuilder.kt @@ -281,7 +281,7 @@ class ScaleProviderBuilder(private val aes: Aes) { with.labels(myLabels) } if (myLabelFormat != null) { - with.labelFormatter(StringFormat.create(myLabelFormat)::format) + with.labelFormatter(StringFormat.forOneArg(myLabelFormat)::format) } if (myMultiplicativeExpand != null) { with.multiplicativeExpand(myMultiplicativeExpand) diff --git a/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/tooltip/ConstantValue.kt b/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/tooltip/ConstantValue.kt index b35c29dba0e..91d618d1b24 100644 --- a/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/tooltip/ConstantValue.kt +++ b/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/tooltip/ConstantValue.kt @@ -15,7 +15,7 @@ class ConstantValue( ) : ValueSource { private val myDataValue = if (format != null) { - StringFormat.create(format).format(value) + StringFormat.forOneArg(format).format(value) } else { value.toString() } diff --git a/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/tooltip/DataFrameValue.kt b/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/tooltip/DataFrameValue.kt index ae6940855bd..828b3c5423c 100644 --- a/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/tooltip/DataFrameValue.kt +++ b/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/tooltip/DataFrameValue.kt @@ -19,9 +19,6 @@ class DataFrameValue( private lateinit var myDataFrame: DataFrame private lateinit var myVariable: DataFrame.Variable private val myFormatter = format?.let { -// StringFormat(format).also { -// require(it.argsNumber == 1) { "Wrong number of arguments in pattern \'$format\' to format \'$name\'. Expected 1 argument instead of ${it.argsNumber}" } -// } StringFormat.forOneArg(format, formatFor = name) } diff --git a/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/tooltip/MappingValue.kt b/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/tooltip/MappingValue.kt index 2109170540c..b74f24f46cb 100644 --- a/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/tooltip/MappingValue.kt +++ b/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/tooltip/MappingValue.kt @@ -21,9 +21,6 @@ class MappingValue( private lateinit var myDataAccess: MappedDataAccess private var myDataLabel: String? = null private val myFormatter = format?.let { -// StringFormat(format).also { -// require(it.argsNumber == 1) { "Wrong number of arguments in pattern \'$format\' to format \'${aes.name}\'. Expected 1 argument instead of ${it.argsNumber}" } -// } StringFormat.forOneArg(format, formatFor = aes.name) } diff --git a/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/tooltip/TooltipLine.kt b/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/tooltip/TooltipLine.kt index 74abc8c3674..9b7c9f65e5c 100644 --- a/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/tooltip/TooltipLine.kt +++ b/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/tooltip/TooltipLine.kt @@ -18,10 +18,7 @@ class TooltipLine( ) : TooltipLineSpec { constructor(other: TooltipLine) : this(other.label, other.pattern, other.fields.map(ValueSource::copy)) - // private val myLineFormatter = StringFormat(pattern, STRING_FORMAT).also { -// require(it.argsNumber == fields.size) { "Wrong number of arguments in pattern \'$pattern\' to format fields. Expected ${fields.size} arguments instead of ${it.argsNumber}" } -// } - private val myLineFormatter = StringFormat.forNArgs(pattern, STRING_FORMAT, fields.size, "fields") + private val myLineFormatter = StringFormat.forNArgs(pattern, fields.size, "fields") fun initDataContext(dataContext: DataContext) { fields.forEach { it.initDataContext(dataContext) } diff --git a/plot-common-portable/src/commonMain/kotlin/jetbrains/datalore/plot/common/text/DateTimeFormatUtil.kt b/plot-common-portable/src/commonMain/kotlin/jetbrains/datalore/plot/common/text/DateTimeFormatUtil.kt index b6c4b357bfa..13b7a79d13e 100644 --- a/plot-common-portable/src/commonMain/kotlin/jetbrains/datalore/plot/common/text/DateTimeFormatUtil.kt +++ b/plot-common-portable/src/commonMain/kotlin/jetbrains/datalore/plot/common/text/DateTimeFormatUtil.kt @@ -5,13 +5,13 @@ package jetbrains.datalore.plot.common.text -import jetbrains.datalore.base.dateFormat.Format +import jetbrains.datalore.base.dateFormat.DateTimeFormat import jetbrains.datalore.base.datetime.Instant import jetbrains.datalore.base.datetime.tz.TimeZone object DateTimeFormatUtil { fun formatDateUTC(instant: Number, pattern: String): String { - val format = Format(pattern) + val format = DateTimeFormat(pattern) return instant.toLong() .let(::Instant) .let(TimeZone.UTC::toDateTime) diff --git a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/FacetConfig.kt b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/FacetConfig.kt index 2f36e0ca27a..3eadef5011c 100644 --- a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/FacetConfig.kt +++ b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/FacetConfig.kt @@ -138,7 +138,7 @@ internal class FacetConfig(options: Map) : OptionsAccessor(options) return when (optionVal) { null -> DEF_FORMATTER else -> { - val fmt = StringFormat.create(optionVal.toString()) + val fmt = StringFormat.forOneArg(optionVal.toString()) return { value: Any -> fmt.format(value) } } } diff --git a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/GeomProtoClientSide.kt b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/GeomProtoClientSide.kt index 2b096b900fd..6b90a0aaf28 100644 --- a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/GeomProtoClientSide.kt +++ b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/GeomProtoClientSide.kt @@ -132,9 +132,6 @@ class GeomProtoClientSide(geomKind: GeomKind) : GeomProto(geomKind) { if (labelFormat != null) { geom.formatter = StringFormat.forOneArg(labelFormat) -// require(geom.formatter!!.argsNumber == 1) { -// "Wrong number of arguments in pattern \'$labelFormat\' to format label. Expected 1 argument instead of ${geom.formatter!!.argsNumber}" -// } } else { throw IllegalArgumentException("Expected: label_format = 'format string'") } diff --git a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/ScaleConfig.kt b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/ScaleConfig.kt index f1107e1185c..c424f6bea12 100644 --- a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/ScaleConfig.kt +++ b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/ScaleConfig.kt @@ -5,6 +5,9 @@ package jetbrains.datalore.plot.config +import jetbrains.datalore.base.gcommon.base.Preconditions.checkArgument +import jetbrains.datalore.base.stringFormat.StringFormat +import jetbrains.datalore.base.stringFormat.StringFormat.FormatType.DATETIME_FORMAT import jetbrains.datalore.base.values.Color import jetbrains.datalore.plot.base.Aes import jetbrains.datalore.plot.base.scale.Mappers.nullable @@ -162,7 +165,10 @@ class ScaleConfig(options: Map) : OptionsAccessor(options) { b.discreteDomainReverse(reverse) if (getBoolean(Option.Scale.DATE_TIME)) { - val dateTimeFormatter = getString(FORMAT)?.let { Formatter.time(it) } + val dateTimeFormatter = getString(FORMAT)?.let { pattern -> + val stringFormat = StringFormat.forOneArg(pattern, type = DATETIME_FORMAT) + return@let { value: Any -> stringFormat.format(value) } + } b.breaksGenerator(DateTimeBreaksGen(dateTimeFormatter)) } else if (!discreteDomain && has(Option.Scale.CONTINUOUS_TRANSFORM)) { val transformName = getStringSafe(Option.Scale.CONTINUOUS_TRANSFORM) diff --git a/plot-config/src/jvmTest/kotlin/plot/config/ScaleConfigLabelsTest.kt b/plot-config/src/jvmTest/kotlin/plot/config/ScaleConfigLabelsTest.kt index 49a80ddffc9..acc2c3f9062 100644 --- a/plot-config/src/jvmTest/kotlin/plot/config/ScaleConfigLabelsTest.kt +++ b/plot-config/src/jvmTest/kotlin/plot/config/ScaleConfigLabelsTest.kt @@ -5,10 +5,7 @@ package jetbrains.datalore.plot.config -import jetbrains.datalore.base.datetime.Date -import jetbrains.datalore.base.datetime.DateTime -import jetbrains.datalore.base.datetime.Month -import jetbrains.datalore.base.datetime.Time +import jetbrains.datalore.base.datetime.* import jetbrains.datalore.base.datetime.tz.TimeZone import jetbrains.datalore.base.gcommon.collect.ClosedRange import jetbrains.datalore.plot.base.Aes @@ -209,6 +206,37 @@ class ScaleConfigLabelsTest { assertEquals(listOf("January 2021"), yLabels) } + @Test + fun `DateTime format should be applied to the breaks`() { + val instants = List(3) { + DateTime(Date(1, Month.JANUARY, 2021)).add(Duration.DAY.mul(it.toLong())) + }.map { TimeZone.UTC.toInstant(it).timeSinceEpoch.toDouble() } + + val scaleMap = getScaleMap( + data = mapOf( + "value" to listOf(instants.first()) + ), + mapping = mappingXY, + scales = listOf( + mapOf( + Option.Scale.AES to Aes.X.name, + DATE_TIME to true, + FORMAT to "%d-%m-%Y", + BREAKS to instants + ) + ) + ) + + val xLabels = getScaleLabels( + scaleMap[Aes.X], + targetCount = 1, + closeRange = ClosedRange(instants.first(), instants.last()) + ) + assertEquals( + expected = listOf("01-01-2021","02-01-2021","03-01-2021"), + xLabels) + } + @Test fun `set format for the non positional scale`() { val data = mapOf("value" to listOf(1, 2, 3), "c" to listOf("red", "green", "blue")) diff --git a/plot-config/src/jvmTest/kotlin/plot/config/TooltipConfigTest.kt b/plot-config/src/jvmTest/kotlin/plot/config/TooltipConfigTest.kt index 5005a2c17b4..e887c7156b0 100644 --- a/plot-config/src/jvmTest/kotlin/plot/config/TooltipConfigTest.kt +++ b/plot-config/src/jvmTest/kotlin/plot/config/TooltipConfigTest.kt @@ -796,6 +796,33 @@ class TooltipConfigTest { } } + @Test + fun `format() should understand DateTime format`() { + val geomLayer = buildPointLayer( + data = mapOf( + "x" to listOf(1609459200000), + "y" to listOf(0.0) + ), + mapping = mapOf( + Aes.X.name to "x", + Aes.Y.name to "y", + ), + tooltips = mapOf( + TOOLTIP_LINES to listOf("^x"), + TOOLTIP_FORMATS to listOf( + mapOf( + FIELD to "^x", + FORMAT to "%d.%m.%y" + ) + ) + ) + ) + assertTooltipStrings( + expected = listOf("01.01.21"), + actual = getGeneralTooltipStrings(geomLayer) + ) + } + companion object { private fun getGeneralTooltipStrings(geomLayer: GeomLayer): List { return getGeneralTooltipLines(geomLayer).map(Line::toString)