Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions benchmarks/src/jmh/kotlin/ParallelFormatBenchmark.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright 2019-2025 JetBrains s.r.o. and contributors.
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
*/

@file:Suppress("unused")

package kotlinx.datetime

import kotlinx.datetime.format.alternativeParsing
import kotlinx.datetime.format.char
import org.openjdk.jmh.annotations.*
import org.openjdk.jmh.infra.Blackhole
import java.util.concurrent.TimeUnit

@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 1)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
@Fork(1)
open class ParallelFormatBenchmark {

@Param("2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12")
var n = 0

@Benchmark
fun formatCreationWithAlternativeParsing(blackhole: Blackhole) {
val format = LocalDateTime.Format {
repeat(n) {
alternativeParsing(
{ monthNumber() },
{ day() },
primaryFormat = { hour() }
)
char('@')
minute()
char('#')
second()
}
}
blackhole.consume(format)
}

@Benchmark
fun formatCreationWithNestedAlternativeParsing(blackhole: Blackhole) {
val format = LocalDateTime.Format {
repeat(n) { index ->
alternativeParsing(
{ monthNumber(); char('-'); day() },
{ day(); char('/'); monthNumber() },
primaryFormat = { year(); char('-'); monthNumber(); char('-'); day() }
)

if (index and 1 == 0) {
alternativeParsing(
{
alternativeParsing(
{ hour(); char(':'); minute() },
{ minute(); char(':'); second() },
primaryFormat = { hour(); char(':'); minute(); char(':'); second() }
)
},
primaryFormat = {
year(); char('-'); monthNumber(); char('-'); day()
char('T')
hour(); char(':'); minute(); char(':'); second()
}
)
}

char('|')
if (index % 3 == 0) {
char('|')
}

if (index and 2 == 0) {
alternativeParsing(
{ char('Z') },
{ char('+'); hour(); char(':'); minute() },
primaryFormat = { char('-'); hour(); char(':'); minute() }
)
}
}
}
blackhole.consume(format)
}
}
47 changes: 47 additions & 0 deletions benchmarks/src/jmh/kotlin/PythonDateTimeFormatBenchmark.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright 2019-2025 JetBrains s.r.o. and contributors.
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
*/

@file:Suppress("unused")

package kotlinx.datetime

import kotlinx.datetime.format.char
import kotlinx.datetime.format.optional
import org.openjdk.jmh.annotations.*
import org.openjdk.jmh.infra.Blackhole
import java.util.concurrent.*

@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 1)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
@Fork(1)
open class PythonDateTimeFormatBenchmark {

@Benchmark
fun buildPythonDateTimeFormat(blackhole: Blackhole) {
val v = LocalDateTime.Format {
year()
char('-')
monthNumber()
char('-')
day()
char(' ')
hour()
char(':')
minute()
optional {
char(':')
second()
optional {
char('.')
secondFraction()
}
}
}
blackhole.consume(v)
}
}
45 changes: 45 additions & 0 deletions benchmarks/src/jmh/kotlin/SerialFormatBenchmark.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright 2019-2025 JetBrains s.r.o. and contributors.
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
*/

@file:Suppress("unused")

package kotlinx.datetime

import kotlinx.datetime.format.char
import org.openjdk.jmh.annotations.*
import org.openjdk.jmh.infra.Blackhole
import java.util.concurrent.TimeUnit

@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 1)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
@Fork(1)
open class SerialFormatBenchmark {

@Param("1", "2", "4", "8", "16", "32", "64", "128", "256", "512", "1024")
var n = 0

@Benchmark
fun largeSerialFormat(blackhole: Blackhole) {
val format = LocalDateTime.Format {
repeat(n) {
char('^')
monthNumber()
char('&')
day()
char('!')
hour()
char('$')
minute()
char('#')
second()
char('@')
}
}
blackhole.consume(format)
}
}
41 changes: 41 additions & 0 deletions core/common/src/internal/format/parser/ConcatenatedListView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright 2019-2025 JetBrains s.r.o. and contributors.
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
*/

package kotlinx.datetime.internal.format.parser

internal class ConcatenatedListView<T>(val list1: List<T>, val list2: List<T>) : AbstractList<T>() {
override val size: Int
get() = list1.size + list2.size

override fun get(index: Int): T = if (index < list1.size) list1[index] else list2[index - list1.size]

override fun iterator(): Iterator<T> = ConcatenatedListViewIterator()

private inner class ConcatenatedListViewIterator : Iterator<T> {
private val iterators: List<Iterator<T>> = buildList {
collectIterators(list1)
collectIterators(list2)
}
private var index = 0

private fun MutableList<Iterator<T>>.collectIterators(list: List<T>) {
if (list is ConcatenatedListView<T>) {
collectIterators(list.list1)
collectIterators(list.list2)
} else {
add(list.iterator())
}
}

override fun hasNext(): Boolean {
while (index < iterators.size && !iterators[index].hasNext()) {
index++
}
return index < iterators.size
}

override fun next(): T = iterators[index].next()
}
}
131 changes: 67 additions & 64 deletions core/common/src/internal/format/parser/Parser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -44,83 +44,86 @@ internal class ParserStructure<in Output>(
// TODO: O(size of the resulting parser ^ 2), but can be O(size of the resulting parser)
internal fun <T> List<ParserStructure<T>>.concat(): ParserStructure<T> {
fun <T> ParserStructure<T>.append(other: ParserStructure<T>): ParserStructure<T> = if (followedBy.isEmpty()) {
ParserStructure(operations + other.operations, other.followedBy)
ParserStructure(ConcatenatedListView(operations, other.operations), other.followedBy)
} else {
ParserStructure(operations, followedBy.map { it.append(other) })
}

fun <T> ParserStructure<T>.simplify(unconditionalModifications: List<UnconditionalModification<T>>): ParserStructure<T> {
val newOperations = mutableListOf<ParserOperation<T>>()
var currentNumberSpan: MutableList<NumberConsumer<T>>? = null
val unconditionalModificationsForTails = unconditionalModifications.toMutableList()
// joining together the number consumers in this parser before the first alternative;
// collecting the unconditional modifications to push them to the end of all the parser's branches.
for (op in operations) {
if (op is NumberSpanParserOperation) {
if (currentNumberSpan != null) {
currentNumberSpan.addAll(op.consumers)
val cache = hashMapOf<Pair<ParserStructure<T>, List<UnconditionalModification<T>>>, ParserStructure<T>>()

fun ParserStructure<T>.simplify(unconditionalModifications: List<UnconditionalModification<T>>): ParserStructure<T> =
cache.getOrPut(this to unconditionalModifications) {
val newOperations = mutableListOf<ParserOperation<T>>()
var currentNumberSpan: MutableList<NumberConsumer<T>>? = null
val unconditionalModificationsForTails = unconditionalModifications.toMutableList()
// joining together the number consumers in this parser before the first alternative;
// collecting the unconditional modifications to push them to the end of all the parser's branches.
for (op in operations) {
if (op is NumberSpanParserOperation) {
if (currentNumberSpan != null) {
currentNumberSpan.addAll(op.consumers)
} else {
currentNumberSpan = op.consumers.toMutableList()
}
} else if (op is UnconditionalModification) {
unconditionalModificationsForTails.add(op)
} else {
currentNumberSpan = op.consumers.toMutableList()
}
} else if (op is UnconditionalModification) {
unconditionalModificationsForTails.add(op)
} else {
if (currentNumberSpan != null) {
newOperations.add(NumberSpanParserOperation(currentNumberSpan))
currentNumberSpan = null
if (currentNumberSpan != null) {
newOperations.add(NumberSpanParserOperation(currentNumberSpan))
currentNumberSpan = null
}
newOperations.add(op)
}
newOperations.add(op)
}
}
val mergedTails = followedBy.flatMap {
val simplified = it.simplify(unconditionalModificationsForTails)
// parser `ParserStructure(emptyList(), p)` is equivalent to `p`,
// unless `p` is empty. For example, ((a|b)|(c|d)) is equivalent to (a|b|c|d).
// As a special case, `ParserStructure(emptyList(), emptyList())` represents a parser that recognizes an empty
// string. For example, (|a|b) is not equivalent to (a|b).
if (simplified.operations.isEmpty())
simplified.followedBy.ifEmpty { listOf(simplified) }
else
listOf(simplified)
}.ifEmpty {
// preserving the invariant that `mergedTails` contains all unconditional modifications
listOf(ParserStructure(unconditionalModificationsForTails, emptyList()))
}
return if (currentNumberSpan == null) {
// the last operation was not a number span, or it was a number span that we are allowed to interrupt
ParserStructure(newOperations, mergedTails)
} else if (mergedTails.none {
it.operations.firstOrNull()?.let { it is NumberSpanParserOperation } == true
}) {
// the last operation was a number span, but there are no alternatives that start with a number span.
newOperations.add(NumberSpanParserOperation(currentNumberSpan))
ParserStructure(newOperations, mergedTails)
} else {
val newTails = mergedTails.map {
when (val firstOperation = it.operations.firstOrNull()) {
is NumberSpanParserOperation -> {
ParserStructure(
listOf(NumberSpanParserOperation(currentNumberSpan + firstOperation.consumers)) + it.operations.drop(
1
),
val mergedTails = followedBy.flatMap {
val simplified = it.simplify(unconditionalModificationsForTails)
// parser `ParserStructure(emptyList(), p)` is equivalent to `p`,
// unless `p` is empty. For example, ((a|b)|(c|d)) is equivalent to (a|b|c|d).
// As a special case, `ParserStructure(emptyList(), emptyList())` represents a parser that recognizes an empty
// string. For example, (|a|b) is not equivalent to (a|b).
if (simplified.operations.isEmpty())
simplified.followedBy.ifEmpty { listOf(simplified) }
else
listOf(simplified)
}.ifEmpty {
// preserving the invariant that `mergedTails` contains all unconditional modifications
listOf(ParserStructure(unconditionalModificationsForTails, emptyList()))
}
if (currentNumberSpan == null) {
// the last operation was not a number span, or it was a number span that we are allowed to interrupt
ParserStructure(newOperations, mergedTails)
} else if (mergedTails.none {
it.operations.firstOrNull()?.let { it is NumberSpanParserOperation } == true
}) {
// the last operation was a number span, but there are no alternatives that start with a number span.
newOperations.add(NumberSpanParserOperation(currentNumberSpan))
ParserStructure(newOperations, mergedTails)
} else {
val newTails = mergedTails.map {
when (val firstOperation = it.operations.firstOrNull()) {
is NumberSpanParserOperation -> {
ParserStructure(
listOf(NumberSpanParserOperation(currentNumberSpan + firstOperation.consumers)) + it.operations.drop(
1
),
it.followedBy
)
}

null -> ParserStructure(
listOf(NumberSpanParserOperation(currentNumberSpan)),
it.followedBy
)
}

null -> ParserStructure(
listOf(NumberSpanParserOperation(currentNumberSpan)),
it.followedBy
)

else -> ParserStructure(
listOf(NumberSpanParserOperation(currentNumberSpan)) + it.operations,
it.followedBy
)
else -> ParserStructure(
listOf(NumberSpanParserOperation(currentNumberSpan)) + it.operations,
it.followedBy
)
}
}
ParserStructure(newOperations, newTails)
}
ParserStructure(newOperations, newTails)
}
}
val naiveParser = foldRight(ParserStructure<T>(emptyList(), emptyList())) { parser, acc -> parser.append(acc) }
return naiveParser.simplify(emptyList())
}
Expand Down