Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DateTime format support in tooltips #421

Merged
merged 8 commits into from
Aug 18, 2021
Next Next commit
Add support of DateTime format to StringFormat.
Add tests.
  • Loading branch information
OLarionova-HORIS committed Aug 16, 2021
commit bd1d0b87b9850d33d801132c29248d970edea56a
Original file line number Diff line number Diff line change
Expand Up @@ -5,63 +5,60 @@

package jetbrains.datalore.base.stringFormat

import jetbrains.datalore.base.dateFormat.Format
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,
val formatType: FormatType
) {
enum class FormatType {
NUMBER_FORMAT,
STRING_FORMAT
STRING_FORMAT,
DATETIME_FORMAT
}

private val myNumberFormatters: List<NumberFormat?>
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<Any>): 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("{{", "{")
Expand All @@ -70,12 +67,48 @@ 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 null
}
when (formatType) {
NUMBER_FORMAT -> {
val numberFormatter: NumberFormat =
try {
NumberFormat(formatPattern)
} catch (e: Exception) {
error("Wrong number pattern: $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 = Format(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)
}
}

Expand Down Expand Up @@ -109,18 +142,25 @@ class StringFormat private constructor(
return create(pattern, type, formatFor, argCount)
}

private fun detectFormatType(pattern: String): FormatType {
fun isDateTimeFormatPattern(pattern: String): Boolean {
return Format.parse(pattern).find { it is Format.PatternSpecPart } != null
}

return when {
NumberFormat.isValidPattern(pattern) -> NUMBER_FORMAT
isDateTimeFormatPattern(pattern) -> DATETIME_FORMAT
else -> STRING_FORMAT
}
}

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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -119,10 +128,86 @@ class StringFormatTest {
@Test
fun `try to format static text as number format`() {
val exception = assertFailsWith(IllegalStateException::class) {
StringFormat.create("text", type = NUMBER_FORMAT).format(emptyList())
StringFormat.create("pattern", type = NUMBER_FORMAT).format("text")
}
assertEquals(
"Wrong number 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("%d.%m.%y").format("01.01.2000")
}
assertEquals(
"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(
"Wrong number pattern: text",
"Can't detect type of pattern 'PP' used in string pattern '{.1f} x {PP}'",
exception.message
)
}
Expand Down