diff --git a/js/js.libraries/src/core/regex.kt b/js/js.libraries/src/core/regex.kt index 84d575483f9..636cea1f8cb 100644 --- a/js/js.libraries/src/core/regex.kt +++ b/js/js.libraries/src/core/regex.kt @@ -16,7 +16,7 @@ package kotlin.text -private val TODO: Nothing get() = throw java.lang.UnsupportedOperationException() +import java.util.ArrayList public enum class RegexOption(val value: String) { @@ -45,16 +45,55 @@ public class Regex(pattern: String, options: Set) { public fun matchAll(input: CharSequence): Sequence = sequence({ match(input) }, { match -> match.next() }) public fun replace(input: CharSequence, replacement: String): String = input.toString().nativeReplace(nativePattern, replacement) - public fun replace(input: CharSequence, evaluator: (MatchResult) -> String): String = TODO - public fun split(input: CharSequence, limit: Int = 0): List = TODO + public inline fun replace(input: CharSequence, transform: (MatchResult) -> String): String { + var match = match(input) + if (match == null) return input.toString() + + var lastStart = 0 + val length = input.length() + val sb = StringBuilder(length) + do { + val foundMatch = match!! + sb.append(input, lastStart, foundMatch.range.start) + sb.append(transform(foundMatch)) + lastStart = foundMatch.range.end + 1 + match = foundMatch.next() + } while (lastStart < length && match != null) + + if (lastStart < length) { + sb.append(input, lastStart, length) + } + + return sb.toString() + } + + public fun replaceFirst(input: CharSequence, replacement: String): String = + input.toString().nativeReplace(RegExp(pattern, options.map { it.value }.joinToString()), replacement) + + public fun split(input: CharSequence, limit: Int = 0): List { + require(limit >= 0, { "Limit must be non-negative, but was $limit" } ) + val matches = matchAll(input).let { if (limit == 0) it else it.take(limit - 1) } + val result = ArrayList() + var lastStart = 0 + + for (match in matches) { + result.add(input.subSequence(lastStart, match.range.start).toString()) + lastStart = match.range.end + 1 + } + result.add(input.subSequence(lastStart, input.length()).toString()) + return result + } public override fun toString(): String = nativePattern.toString() companion object { public fun fromLiteral(literal: String): Regex = Regex(escape(literal)) - public fun escape(literal: String): String = TODO - public fun escapeReplacement(literal: String): String = literal.nativeReplace(RegExp("\\$", "g"), "$$$$") + public fun escape(literal: String): String = literal.nativeReplace(patternEscape, "\\$&") + public fun escapeReplacement(literal: String): String = literal.nativeReplace(replacementEscape, "$$$$") + + private val patternEscape = RegExp("""[-\\^$*+?.()|[\]{}]""", "g") + private val replacementEscape = RegExp("""\$""", "g") } } diff --git a/libraries/stdlib/src/kotlin/io/files/FilePathComponents.kt b/libraries/stdlib/src/kotlin/io/files/FilePathComponents.kt index 8d23033b9d0..26004713e35 100644 --- a/libraries/stdlib/src/kotlin/io/files/FilePathComponents.kt +++ b/libraries/stdlib/src/kotlin/io/files/FilePathComponents.kt @@ -3,6 +3,7 @@ package kotlin.io import java.io.File import java.util.Collections import java.util.NoSuchElementException +import kotlin.text.Regex /** * Estimation of a root name by a given file name. @@ -95,7 +96,7 @@ public fun File.filePathComponents(): FilePathComponents { // Split not only by / or \, but also by //, ///, \\, \\\, etc. val list = if (rootName.length() > 0 && subPath.isEmpty()) listOf() else // Looks awful but we split just by /+ or \+ depending on OS - subPath.split("""\Q${File.separatorChar}\E+""".toPattern()).toList().map { it -> File(it) } + subPath.split(Regex.fromLiteral(File.separatorChar.toString())).toList().map { it -> File(it) } return FilePathComponents(rootName, list) } diff --git a/libraries/stdlib/src/kotlin/text/regex/RegexJVM.kt b/libraries/stdlib/src/kotlin/text/regex/RegexJVM.kt index 2d78ff4f050..7927cc04a05 100644 --- a/libraries/stdlib/src/kotlin/text/regex/RegexJVM.kt +++ b/libraries/stdlib/src/kotlin/text/regex/RegexJVM.kt @@ -19,8 +19,6 @@ package kotlin.text import java.util.regex.Pattern import java.util.regex.Matcher -private val TODO: Nothing get() = throw UnsupportedOperationException() - public trait FlagEnum { public val value: Int public val mask: Int @@ -65,9 +63,35 @@ public class Regex( /* visibility? */ val nativePattern: Pattern) { public fun matchAll(input: CharSequence): Sequence = sequence({ match(input) }, { match -> match.next() }) public fun replace(input: CharSequence, replacement: String): String = nativePattern.matcher(input).replaceAll(replacement) - public fun replace(input: CharSequence, evaluator: (MatchResult) -> String): String = TODO - public fun split(input: CharSequence, limit: Int = 0): List = nativePattern.split(input, limit).asList() // TODO: require(limit>=0) + public fun replaceFirst(input: CharSequence, replacement: String): String = nativePattern.matcher(input).replaceFirst(replacement) + + public inline fun replace(input: CharSequence, transform: (MatchResult) -> String): String { + var match = match(input) + if (match == null) return input.toString() + + var lastStart = 0 + val length = input.length() + val sb = StringBuilder(length) + do { + val foundMatch = match!! + sb.append(input, lastStart, foundMatch.range.start) + sb.append(transform(foundMatch)) + lastStart = foundMatch.range.end + 1 + match = foundMatch.next() + } while (lastStart < length && match != null) + + if (lastStart < length) { + sb.append(input, lastStart, length) + } + + return sb.toString() + } + + public fun split(input: CharSequence, limit: Int = 0): List { + require(limit >= 0, { "Limit must be non-negative, but was $limit" } ) + return nativePattern.split(input, if (limit == 0) -1 else limit).asList() + } public override fun toString(): String = nativePattern.toString() diff --git a/libraries/stdlib/test/text/RegexTest.kt b/libraries/stdlib/test/text/RegexTest.kt index 3a94a5691ac..fa1aba560bd 100644 --- a/libraries/stdlib/test/text/RegexTest.kt +++ b/libraries/stdlib/test/text/RegexTest.kt @@ -71,4 +71,38 @@ class RegexTest { assertEquals("bye", m2.groups[2]?.value) } + test fun escapeLiteral() { + val literal = """[-\/\\^$*+?.()|[\]{}]""" + assertTrue(Regex.fromLiteral(literal).matches(literal)) + assertTrue(Regex(Regex.escape(literal)).matches(literal)) + } + + test fun replace() { + val input = "123-456" + val pattern = "(\\d+)".toRegex() + assertEquals("(123)-(456)", pattern.replace(input, "($1)")) + + } + + test fun replaceEvaluator() { + val input = "/12/456/7890/" + val pattern = "\\d+".toRegex() + assertEquals("/2/3/4/", pattern.replace(input, { it.value.length().toString() } )) + } + + + test fun split() { + val input = """ + some ${"\t"} word + split + """.trim() + + assertEquals(listOf("some", "word", "split"), "\\s+".toRegex().split(input)) + + assertEquals(listOf("name", "value=5"), "=".toRegex().split("name=value=5", limit = 2)) + + } + + + } \ No newline at end of file