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
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))
}
}