From 54dde5f1f7fee75099e8bc742e47930f2dd5c304 Mon Sep 17 00:00:00 2001 From: "alexander.nutz" Date: Tue, 2 Apr 2024 15:47:10 +0200 Subject: [PATCH] parser combinator, json parser, future, other shit --- README.md | 35 ++++++- src/main/kotlin/blitz/async/Async.kt | 14 +++ src/main/kotlin/blitz/async/Future.kt | 61 ++++++++++++ src/main/kotlin/blitz/collections/Skip.kt | 10 ++ src/main/kotlin/blitz/parse/JSON.kt | 80 ++++++++++++++++ src/main/kotlin/blitz/parse/NumParse.kt | 43 +++++++++ src/main/kotlin/blitz/parse/comb/Parser.kt | 105 +++++++++++++++++++++ src/main/kotlin/blitz/term/AnsiiStr.kt | 2 +- src/main/kotlin/blitz/term/ProgressBar.kt | 46 +++++++++ src/main/kotlin/blitz/term/Terminal.kt | 45 +++++++++ 10 files changed, 439 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/blitz/async/Async.kt create mode 100644 src/main/kotlin/blitz/async/Future.kt create mode 100644 src/main/kotlin/blitz/collections/Skip.kt create mode 100644 src/main/kotlin/blitz/parse/JSON.kt create mode 100644 src/main/kotlin/blitz/parse/NumParse.kt create mode 100644 src/main/kotlin/blitz/parse/comb/Parser.kt create mode 100644 src/main/kotlin/blitz/term/ProgressBar.kt diff --git a/README.md b/README.md index f1695cd..11256f7 100644 --- a/README.md +++ b/README.md @@ -200,9 +200,42 @@ val out = MutMultiColoredMultiLineString(fill = ColoredChar(' ')) out.box(x to y, 20 to 5, BoxDrawingCharSet.ROUND, Terminal.COLORS.WHITE.brighter.fg) println(out.toString()) ``` +### Progress bar +```kotlin +val prog = ProgressBar(100, ProgressBar.Style.BASIC) +repeat(100) { + prog.step(it) + Thread.sleep(10) +} +prog.step(100) +prog.end() +``` +### Parser combinator +```kotlin +val jsonArray = parser { + it.require("[") + ?.array(",") { elem -> + elem.whitespaces() + .map(NumParse.int) + ?.whitespaces() + } + ?.require("]") +} +println(jsonArray(Parsable("[ 1, 2,3 , 4]"))) +``` +### JSON parser +```kotlin +val json = """ +{ + "a": 1, + "b": { "1": 1, "2": 2 }, + "c": [1,2] +} +""" +println(JSON.parse(json)!!.obj["b"]!!.obj["1"]!!.num) +``` ### Either No example yet - ### Tree No example yet \ No newline at end of file diff --git a/src/main/kotlin/blitz/async/Async.kt b/src/main/kotlin/blitz/async/Async.kt new file mode 100644 index 0000000..54c9820 --- /dev/null +++ b/src/main/kotlin/blitz/async/Async.kt @@ -0,0 +1,14 @@ +package blitz.async + +import kotlin.random.Random + +// TODO: use coroutines? +fun async(fn: () -> R): Future { + lateinit var future: ThreadWaitingFuture + val thread = Thread { + future.done(fn()) + } + future = ThreadWaitingFuture(thread) + thread.start() + return future +} \ No newline at end of file diff --git a/src/main/kotlin/blitz/async/Future.kt b/src/main/kotlin/blitz/async/Future.kt new file mode 100644 index 0000000..3eca711 --- /dev/null +++ b/src/main/kotlin/blitz/async/Future.kt @@ -0,0 +1,61 @@ +package blitz.async + +import blitz.Obj +import java.util.concurrent.atomic.AtomicBoolean + +open class Future { + protected lateinit var value: Obj + protected val then = mutableListOf<(T) -> Unit>() + protected val presentAtomic = AtomicBoolean(false) + + val present get() = presentAtomic.get() + + fun done(va: T) { + if (presentAtomic.get()) + error("Can not set future twice!") + value = Obj.of(va) + presentAtomic.set(true) + then.forEach { + it(va) + } + } + + fun then(fn: (T) -> Unit): Future { + if (presentAtomic.get()) + fn(value.v) + else + then.add(fn) + return this + } + + fun map(fn: (T) -> R): Future { + val new = Future() + then { + new.done(fn(it)) + } + return new + } + + // TODO: can do better + open fun await(iterSleepMS: Long = 10): T { + val thread = Thread { + while (true) Thread.sleep(iterSleepMS) + } + thread.start() + then { thread.interrupt() } + kotlin.runCatching { thread.join() } + return value.v + } + + override fun toString(): String = + if (presentAtomic.get()) "Future(${value.v})" else "Future(?)" +} + +class ThreadWaitingFuture( + private val on: Thread +): Future() { + override fun await(iterSleepMS: Long): T { + on.join() + return value.v + } +} \ No newline at end of file diff --git a/src/main/kotlin/blitz/collections/Skip.kt b/src/main/kotlin/blitz/collections/Skip.kt new file mode 100644 index 0000000..e500187 --- /dev/null +++ b/src/main/kotlin/blitz/collections/Skip.kt @@ -0,0 +1,10 @@ +package blitz.collections + +fun UnGettableIterator.skipSpaces(): UnGettableIterator { + var curr = next() + while (curr == ' ' || curr == '\n' || curr == '\r') { + curr = next() + } + unGet() + return this +} \ No newline at end of file diff --git a/src/main/kotlin/blitz/parse/JSON.kt b/src/main/kotlin/blitz/parse/JSON.kt new file mode 100644 index 0000000..ee2d055 --- /dev/null +++ b/src/main/kotlin/blitz/parse/JSON.kt @@ -0,0 +1,80 @@ +package blitz.parse + +import blitz.parse.comb.* + +object JSON { + lateinit var jsonElement: Parser + val jsonNum = parser { + it.map(NumParse.float)?.mapSecond { n -> + Number(n) + } + } + val jsonString = parser { + it.require("\"") + ?.untilRequire("\"") { str -> Str(str) } + } + val jsonArray = parser { + it.require("[") + ?.array(",") { elem -> + elem.whitespaces() + .map(jsonElement) + ?.whitespaces() + } + ?.require("]") + ?.mapSecond { x -> Array(x) } + } + val jsonObj = parser { + it.require("{") + ?.array(",") { elem -> + elem.whitespaces() + .map(jsonString) + ?.mapSecond { it.str } + ?.whitespaces() + ?.require(":") + ?.whitespaces() + ?.map(jsonElement) + ?.whitespaces() + } + ?.require("}") + ?.mapSecond { x -> Obj(x.toMap()) } + } + + init { + jsonElement = (jsonArray or jsonNum or jsonString or jsonObj).trim() + } + + interface Element { + val arr get() = (this as Array).value + val num get() = (this as Number).value + val str get() = (this as Str).value + val obj get() = (this as Obj).value + + fun isArr() = this is Array + fun isNum() = this is Number + fun isStr() = this is Str + fun isObj() = this is Obj + } + + data class Array(val value: List): Element { + override fun toString(): String = + value.joinToString(separator = ", ", prefix = "[", postfix = "]") + } + + data class Number(val value: Double): Element { + override fun toString(): String = + value.toString() + } + + data class Str(val value: String): Element { + override fun toString(): String = + "\"$value\"" + } + + data class Obj(val value: Map): Element { + override fun toString(): String = + value.map { (k, v) -> "\"$k\": $v" }.joinToString(separator = ", ", prefix = "{", postfix = "}") + } + + fun parse(string: String): Element? = + jsonElement(Parsable(string))?.second +} \ No newline at end of file diff --git a/src/main/kotlin/blitz/parse/NumParse.kt b/src/main/kotlin/blitz/parse/NumParse.kt new file mode 100644 index 0000000..1bdfd53 --- /dev/null +++ b/src/main/kotlin/blitz/parse/NumParse.kt @@ -0,0 +1,43 @@ +package blitz.parse + +import blitz.parse.comb.* + +object NumParse { + private val intBase = parser { it.require("0b")?.to(2) } or + parser { it.require("0x")?.to(16) } or + parser { it.require("0o")?.to(8) } or + constantParser(10) + + private val sign = parser { it.require("+")?.to(1) } or + parser { it.require("-")?.to(-1) } or + constantParser(1) + + val int = parser { s -> + s.map(sign)?.map(intBase)?.map { str, (sign, base) -> + val chars = when (base) { + 2 -> "01" + 8 -> "01234567" + 10 -> "0123456789" + 16 -> "0123456789abcdefABCDEF" + else -> error("wtf") + } + str.asLongAs(*chars.toCharArray()) { + it.toLongOrNull(base)?.times(sign) + } + } + } + + val float = parser { s -> + s.map(sign)?.map { str, sign -> + str.asLongAs('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.') { + it.toDoubleOrNull()?.times(sign) + } + } + } +} + +fun parseInt(str: String): Long? = + NumParse.int(Parsable(str))?.second + +fun parseDouble(str: String): Double? = + NumParse.float(Parsable(str))?.second \ No newline at end of file diff --git a/src/main/kotlin/blitz/parse/comb/Parser.kt b/src/main/kotlin/blitz/parse/comb/Parser.kt new file mode 100644 index 0000000..04c270c --- /dev/null +++ b/src/main/kotlin/blitz/parse/comb/Parser.kt @@ -0,0 +1,105 @@ +package blitz.parse.comb + +import blitz.str.collectToString +import kotlin.math.max + +@JvmInline +value class Parsable( + val str: String +) + +typealias Parser = (Parsable) -> Pair? + +fun parser(fn: (Parsable) -> Pair?): Parser = + fn + +fun Parser.trim(): Parser = parser { + it.whitespaces() + .map(this@trim) + ?.whitespaces() +} + +fun constantParser(const: T): Parser = { it to const } + +infix fun Parser.or(other: Parser): Parser = { + this@or(it) ?: other(it) +} + +fun Parsable.spaces(): Parsable { + return Parsable(str.trimStart(' ')) +} + +fun Parsable.whitespaces(): Parsable { + return Parsable(str.trimStart()) +} + +fun Parsable.require(what: String): Parsable? { + if (str.startsWith(what)) + return Parsable(str.substring(what.length)) + return null +} + +fun Parsable.untilRequire(c: String, map: (String) -> T?): Pair? { + return map(str.substringBefore(c))?.let { Parsable(str.substringAfter(c)) to it } +} + +fun Parsable.asLongAs(vararg li: Char, map: (String) -> T?): Pair? { + val o = mutableListOf() + for (c in str) { + if (c in li) + o.add(c) + else + break + } + val out = str.substring(o.size) + return map(o.iterator().collectToString())?.let { Parsable(out) to it } +} + +fun Parsable.map(parser: Parser): Pair? = + parser(this) + +fun Pair.map(fn: (Parsable, T) -> Pair?): Pair? = + fn(first, second) + +fun Pair.map(parser: Parser): Pair>? = + map { parsable, a -> + parser(parsable)?.let { r -> + r.first to (a to r.second) + } + } + +fun Pair.mapFirst(fn: (Parsable) -> Parsable): Pair = + fn(first) to second + +fun Pair.mapFirstNullable(fn: (Parsable) -> Parsable?): Pair? = + fn(first)?.let { it to second } + +fun Pair.mapSecond(fn: (T) -> R): Pair = + first to fn(second) + +fun Pair.spaces(): Pair = + mapFirst { it.spaces() } + +fun Pair.whitespaces(): Pair = + mapFirst { it.whitespaces() } + +fun Pair.require(what: String): Pair? = + mapFirstNullable { it.require(what) } + +fun Parsable.array(sep: String, map: (Parsable) -> Pair?): Pair> { + val out = mutableListOf() + + var curr = str + fun step() = + map(Parsable(curr))?.also { + curr = it.first.str + } + + while (true) { + val r = step() ?: break + out.add(r.second) + curr = (Parsable(curr).require(sep) ?: break).str + } + + return Parsable(curr) to out +} \ No newline at end of file diff --git a/src/main/kotlin/blitz/term/AnsiiStr.kt b/src/main/kotlin/blitz/term/AnsiiStr.kt index 05e9cb2..3671312 100644 --- a/src/main/kotlin/blitz/term/AnsiiStr.kt +++ b/src/main/kotlin/blitz/term/AnsiiStr.kt @@ -13,7 +13,7 @@ class AnsiiMode(internal val values: MutableList) { values.hashCode() } -private val escape = (27).toChar() +internal val escape = (27).toChar() internal fun ansiiStr(str: String, vararg modes: AnsiiMode) = if (modes.isEmpty()) diff --git a/src/main/kotlin/blitz/term/ProgressBar.kt b/src/main/kotlin/blitz/term/ProgressBar.kt new file mode 100644 index 0000000..6819cdf --- /dev/null +++ b/src/main/kotlin/blitz/term/ProgressBar.kt @@ -0,0 +1,46 @@ +package blitz.term + +import kotlin.math.max + +/** + * Single-line progress bar + */ +class ProgressBar( + var max: Int, + val style: Style, + private val preDrawLine: () -> Unit = {}, + private val postDrawLine: () -> Unit = {} +) { + fun end() { + Terminal.Cursor.gotoBeginOfLine() + } + + fun step(pos: Int) { + Terminal.Cursor.gotoBeginOfLine() + preDrawLine() + print(style.pre) + val left = (pos.toDouble() / max * style.len).toInt() + val right = style.len - left + print(style.segment.toString().repeat(max(0, left - 1))) + print(style.segmentCursor) + print(style.segmentEmpty.toString().repeat(right)) + print(style.post) + postDrawLine() + } + + data class Style( + val len: Int, + val pre: String, + val segment: Char, + val segmentCursor: Char, + val segmentEmpty: Char, + val post: String + ) { + companion object { + val BASIC = Style(36, "[ ", '=', '=', ' ', " ]") + val LIGHT = Style(36, "< ", '-', '-', ' ', " >") + val STARS = Style(36, "( ", '*', '*', ' ', " )") + val ARROW = Style(36, "[ ", '=', '>', ' ', " ]") + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/blitz/term/Terminal.kt b/src/main/kotlin/blitz/term/Terminal.kt index c97dfc4..08179b9 100644 --- a/src/main/kotlin/blitz/term/Terminal.kt +++ b/src/main/kotlin/blitz/term/Terminal.kt @@ -17,6 +17,9 @@ object Terminal { val DIM = AnsiiMode(2) val UNDERLINE = AnsiiMode(4) val BLINK = AnsiiMode(5) + val OVERLINE = AnsiiMode(53) + val FRAMED = AnsiiMode(51) + val ENCIRCLED = AnsiiMode(52) } class Color( @@ -84,4 +87,46 @@ object Terminal { fun warn(str: String, vararg modes: AnsiiMode) { errln(str, *modes) } + + object Cursor { + fun savePos() { + print(escape + "7") + } + + fun restorePos() { + print(escape + "8") + } + + fun goto(line: Int, col: Int) { + print("$escape[${line};${col}H") + } + + fun gotoZero() { + print("$escape[H") + } + + fun gotoBeginOfPrevLine() { + print("$escape[F") + } + + fun gotoBeginOfLine() { + print((0x0D).toChar()) + } + } + + fun save() { + print("$escape[?47h") + } + + fun restore() { + print("$escape[?47l") + } + + fun clear() { + print("$escape[2J") + } + + fun clearLine() { + print("$escape[2K") + } } \ No newline at end of file