Skip to content

Commit

Permalink
DateTime format support in tooltips (#421)
Browse files Browse the repository at this point in the history
* Add support of DateTime format to StringFormat.
Add tests.

* Add test: use DateTime format in tooltip format().
Use StringFormat in datetime scales. Add test: apply DateTime format to the breaks.

* Rename Format -> DateTimeFormat.

* Minor code cleanup.

* Minor code cleanup.

* Code cleanup: add function parseOrNull to create NumberFormat.

* Minor changes: new error messages, add function isDateTimeFormat.

* Add notebook with examples of date-time formatting.
  • Loading branch information
OLarionova-HORIS authored Aug 18, 2021
1 parent 7a388f9 commit def7c2d
Show file tree
Hide file tree
Showing 21 changed files with 665 additions and 101 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<SpecPart>) {
class DateTimeFormat(private val spec: List<SpecPart>) {

constructor(spec: String): this(parse(spec))

Expand All @@ -19,7 +19,7 @@ class Format(private val spec: List<SpecPart>) {
}

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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?: " ",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,63 +5,61 @@

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,
val formatType: FormatType
) {
enum class FormatType {
NUMBER_FORMAT,
DATETIME_FORMAT,
STRING_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 +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)
}
}

Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,56 +19,56 @@ 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
fun leadingZeros() {
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))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
Loading

0 comments on commit def7c2d

Please sign in to comment.