Skip to content

Commit 103a24d

Browse files
committed
experimental TOON renderer and DF->TOON converter
1 parent c411a9f commit 103a24d

File tree

3 files changed

+494
-466
lines changed

3 files changed

+494
-466
lines changed
Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
package org.jetbrains.kotlinx.dataframe.impl.io
2+
3+
public enum class ToonDelimiter(internal val value: Char) {
4+
COMMA(','),
5+
TAB('\t'),
6+
PIPE('|'),
7+
}
8+
9+
internal sealed interface ToonElement
10+
11+
internal fun ToonElement.render(
12+
delimiter: ToonDelimiter = ToonDelimiter.COMMA,
13+
indentStart: Int = 0,
14+
indentStep: Int = 2,
15+
): String =
16+
when (this) {
17+
is ToonPrimitive<*> -> render(delimiter)
18+
is ToonArray -> render(delimiter, indentStart, indentStep)
19+
is ToonObject -> render(delimiter, indentStart, indentStep)
20+
}
21+
22+
internal fun ToonElement.asPrimitive() = this as ToonPrimitive<*>
23+
24+
internal fun ToonElement.asObject() = this as ToonObject
25+
26+
internal fun ToonElement.asArray() = this as ToonArray
27+
28+
// region Primitives
29+
30+
internal sealed interface ToonPrimitive<T : Any?> : ToonElement {
31+
32+
val value: T
33+
34+
fun render(delimiter: ToonDelimiter): String
35+
36+
companion object {
37+
val NULL = ToonNull
38+
39+
operator fun invoke(value: Nothing?): ToonNull = ToonNull
40+
41+
operator fun invoke(value: Boolean): ToonBoolean = ToonBoolean(value)
42+
43+
operator fun invoke(value: Number): ToonNumber = ToonNumber(value)
44+
45+
operator fun invoke(value: String): ToonString = ToonString(value)
46+
47+
operator fun invoke(value: Any?): ToonPrimitive<*> =
48+
when (value) {
49+
null -> ToonNull
50+
is Number -> ToonNumber(value)
51+
is Boolean -> ToonBoolean(value)
52+
is String -> ToonString(value)
53+
else -> ToonString(value.toString()) // default to string
54+
}
55+
}
56+
}
57+
58+
internal data object ToonNull : ToonPrimitive<Nothing?> {
59+
override val value: Nothing? = null
60+
61+
override fun render(delimiter: ToonDelimiter): String = "null"
62+
}
63+
64+
@JvmInline
65+
internal value class ToonString(override val value: String) : ToonPrimitive<String> {
66+
67+
private companion object {
68+
private val disallowedToonChars = setOf('\n', '\r', '\t', '"', '{', '}', '[', ']', ':', '\\')
69+
70+
private fun String.looksLikeNumber() =
71+
matches("""/^-?\d+(?:.\d+)?(?:e[+-]?\d+)?$/i""".toRegex()) ||
72+
matches("""/^0\d+$/""".toRegex())
73+
74+
private fun String.needsQuotes(activeDelimiter: ToonDelimiter): Boolean =
75+
isEmpty() ||
76+
trim().length != length ||
77+
this == "true" ||
78+
this == "false" ||
79+
this == "null" ||
80+
looksLikeNumber() ||
81+
any { it in disallowedToonChars } ||
82+
contains(activeDelimiter.value) ||
83+
startsWith('-')
84+
}
85+
86+
override fun render(delimiter: ToonDelimiter): String =
87+
when {
88+
value.needsQuotes(delimiter) -> "\"$value\""
89+
else -> value
90+
}
91+
}
92+
93+
@JvmInline
94+
internal value class ToonNumber(override val value: Number) : ToonPrimitive<Number> {
95+
override fun render(delimiter: ToonDelimiter): String = value.toString()
96+
}
97+
98+
@JvmInline
99+
internal value class ToonBoolean(override val value: Boolean) : ToonPrimitive<Boolean> {
100+
override fun render(delimiter: ToonDelimiter): String = value.toString()
101+
}
102+
103+
// endregion
104+
105+
/**
106+
* id: 123
107+
* name: Ada
108+
* active[2]: true,false
109+
*
110+
* or
111+
*
112+
* user:
113+
* id: 123
114+
* name: Ada
115+
*/
116+
internal class ToonObject(val map: Map<String, ToonElement>) : ToonElement {
117+
// can be made tabular if flat
118+
val isFlat: Boolean = map.values.all { it is ToonPrimitive<*> }
119+
120+
fun String.needsQuotes() = !matches("""^[A-Za-z_][A-Za-z0-9_.]*$""".toRegex())
121+
122+
// TODO? key folding
123+
fun render(delimiter: ToonDelimiter, indentStart: Int, indentStep: Int): String =
124+
buildString {
125+
var isFirst = true
126+
for ((key, value) in map) {
127+
// ignore indents for the first key-value pair
128+
if (isFirst) {
129+
isFirst = false
130+
} else {
131+
append(indent(indentStart))
132+
}
133+
append(
134+
when {
135+
key.needsQuotes() -> "\"$key\""
136+
else -> key
137+
},
138+
)
139+
when (value) {
140+
// id: 123
141+
is ToonPrimitive<*> -> appendLine(": ${value.render(delimiter)}")
142+
143+
// active[2]: true,false
144+
//
145+
// items[2]{sku,qty,price}:
146+
// A1,2,9.99
147+
// B2,1,14.5
148+
//
149+
// items[3]:
150+
// - 1
151+
// - a: 1
152+
// b: 3
153+
// - text
154+
is ToonArray -> append(
155+
value.render(
156+
delimiter = delimiter,
157+
indentStart = indentStart,
158+
indentStep = indentStep,
159+
),
160+
)
161+
162+
// user:
163+
// id: 123
164+
// name: Ada
165+
is ToonObject -> append(
166+
":\n${indent(indentStart + indentStep)}${
167+
value.render(
168+
delimiter = delimiter,
169+
indentStart = indentStart + indentStep,
170+
indentStep = indentStep,
171+
)
172+
}",
173+
)
174+
}
175+
}
176+
}
177+
}
178+
179+
private fun List<ToonObject>.toTabularOrNull(): ToonTabularArray? {
180+
if (isEmpty()) return ToonTabularArray(emptyList(), emptyList())
181+
182+
val header = first().map.keys
183+
if (!all { it.map.keys == header }) return null
184+
if (any { !it.isFlat }) return null
185+
186+
val objectArray = map { it.map.values.toList() as List<ToonPrimitive<*>> }
187+
return ToonTabularArray(header = header.toList(), objectArray = objectArray)
188+
}
189+
190+
internal sealed interface ToonArray : ToonElement {
191+
val size: Int
192+
193+
fun render(delimiter: ToonDelimiter, indentStart: Int, indentStep: Int): String
194+
195+
companion object {
196+
@JvmName("ofPrimitives")
197+
operator fun invoke(primitives: List<ToonPrimitive<*>>): ToonArray = ToonInlineArray(values = primitives)
198+
199+
@JvmName("ofObjectArray")
200+
operator fun invoke(header: List<String>, objectArray: List<List<ToonPrimitive<*>>>): ToonArray {
201+
require(objectArray.all { it.size == header.size }) {
202+
"All object rows must have the same number of columns as the header"
203+
}
204+
return ToonTabularArray(header = header, objectArray = objectArray)
205+
}
206+
207+
@JvmName("ofObjects")
208+
operator fun invoke(objects: List<ToonObject>): ToonArray =
209+
objects.toTabularOrNull()
210+
?: ToonMixedArray(values = objects)
211+
212+
@JvmName("ofElements")
213+
@Suppress("UNCHECKED_CAST")
214+
operator fun invoke(values: List<ToonElement>): ToonArray =
215+
when {
216+
values.all { it is ToonPrimitive<*> } ->
217+
invoke(primitives = values as List<ToonPrimitive<*>>)
218+
219+
values.all { it is ToonObject } -> invoke(objects = values as List<ToonObject>)
220+
221+
else -> ToonMixedArray(values = values)
222+
}
223+
}
224+
}
225+
226+
/**
227+
* tags[3]: admin,ops,dev
228+
*/
229+
private class ToonInlineArray(val values: List<ToonPrimitive<*>>) : ToonArray {
230+
override val size: Int = values.size
231+
232+
override fun render(delimiter: ToonDelimiter, indentStart: Int, indentStep: Int): String =
233+
buildString {
234+
append("[$size")
235+
if (delimiter != ToonDelimiter.COMMA) append(delimiter.value)
236+
append("]: ")
237+
appendLine(
238+
values.joinToString(delimiter.value.toString()) { it.render(delimiter) },
239+
)
240+
}
241+
}
242+
243+
/**
244+
* items[2]{sku,qty,price}:
245+
* A1,2,9.99
246+
* B2,1,14.5
247+
*/
248+
private class ToonTabularArray(val header: List<String>, val objectArray: List<List<ToonPrimitive<*>>>) : ToonArray {
249+
override val size: Int = objectArray.size
250+
251+
override fun render(delimiter: ToonDelimiter, indentStart: Int, indentStep: Int): String =
252+
buildString {
253+
append("[$size")
254+
if (delimiter != ToonDelimiter.COMMA) append(delimiter.value)
255+
append("]{")
256+
append(header.joinToString(delimiter.value.toString()))
257+
appendLine("}:")
258+
259+
val valueIndent = indentStart + indentStep
260+
for (row in objectArray) {
261+
append(indent(valueIndent))
262+
appendLine(
263+
row.joinToString(delimiter.value.toString()) { it.render(delimiter) },
264+
)
265+
}
266+
}
267+
}
268+
269+
/**
270+
* items[3]:
271+
* - 1
272+
* - a: 1
273+
* - text
274+
*/
275+
private class ToonMixedArray(val values: List<ToonElement>) : ToonArray {
276+
override val size: Int = values.size
277+
278+
override fun render(delimiter: ToonDelimiter, indentStart: Int, indentStep: Int): String =
279+
buildString {
280+
append("[$size")
281+
if (delimiter != ToonDelimiter.COMMA) append(delimiter.value)
282+
appendLine("]:")
283+
284+
val valueIndent = indentStart + indentStep
285+
for (value in values) {
286+
append(indent(valueIndent))
287+
append("- ")
288+
append(
289+
value.render(
290+
delimiter = delimiter,
291+
indentStart = valueIndent + indentStep,
292+
indentStep = indentStep,
293+
),
294+
)
295+
if (value is ToonPrimitive<*>) appendLine()
296+
}
297+
}
298+
}
299+
300+
private fun indent(size: Int) = " ".repeat(size)

0 commit comments

Comments
 (0)