diff --git a/js/js.libraries/src/core/regex.kt b/js/js.libraries/src/core/regex.kt index 636cea1f8cb..9fee905b90f 100644 --- a/js/js.libraries/src/core/regex.kt +++ b/js/js.libraries/src/core/regex.kt @@ -18,34 +18,75 @@ package kotlin.text import java.util.ArrayList - +/** + * Provides enumeration values to use to set regular expression options. + */ public enum class RegexOption(val value: String) { + /** Enables case-insensitive matching. */ IGNORE_CASE : RegexOption("i") + /** Enables multiline mode. + * + * In multiline mode the expressions `^` and `$` match just after or just before, + * respectively, a line terminator or the end of the input sequence. */ MULTILINE : RegexOption("m") } +/** + * Represents the results from a single capturing group within a [MatchResult] of [Regex]. + * + * @param value The value of captured group. + */ public data class MatchGroup(val value: String) - +/** A compiled representation of a regular expression. + * + * For pattern syntax reference see [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp] and [http://www.w3schools.com/jsref/jsref_obj_regexp.asp] + */ public class Regex(pattern: String, options: Set) { + /** The pattern string of this regular expression. */ public val pattern: String = pattern + /** The set of options that were used to create this regular expression. */ public val options: Set = options.toSet() private val nativePattern: RegExp = RegExp(pattern, options.map { it.value }.joinToString() + "g") - + /** Indicates whether the regular expression matches the entire [input]. */ public fun matches(input: CharSequence): Boolean { + nativePattern.reset() + val match = nativePattern.exec(input.toString()) + return match != null && (match as RegExpMatch).index == 0 && nativePattern.lastIndex == input.length() + } + + /** Indicates whether the regular expression can find at least a match in the specified [input]. */ + public fun hasMatch(input: CharSequence): Boolean { nativePattern.reset() return nativePattern.test(input.toString()) } - public fun match(input: CharSequence): MatchResult? = nativePattern.findNext(input.toString(), 0) + /** Returns the first match of a regular expression in the [input], beginning at the specified [startIndex]. + * + * @param startIndex An index to start search with, by default 0. Must be not less than zero and not greater than `input.length()` + * @return An instance of [MatchResult] if match was found or `null` otherwise. + */ + public fun match(input: CharSequence, startIndex: Int = 0): MatchResult? = nativePattern.findNext(input.toString(), startIndex) - public fun matchAll(input: CharSequence): Sequence = sequence({ match(input) }, { match -> match.next() }) + /** Returns a sequence of all occurrences of a regular expression within the [input] string, beginning at the specified [startIndex]. + */ + public fun matchAll(input: CharSequence, startIndex: Int = 0): Sequence = sequence({ match(input, startIndex) }, { match -> match.next() }) + /** + * Replaces all occurrences of this regular expression in the specified [input] string with specified [replacement] expression. + * + * @param replacement A replacement expression that can include substitutions. See [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace] for details. + */ public fun replace(input: CharSequence, replacement: String): String = input.toString().nativeReplace(nativePattern, replacement) + /** + * Replaces all occurrences of this regular expression in the specified [input] string with the result of + * the given function [transform] that takes [MatchResult] and returns a string to be used as a + * replacement for that match. + */ public inline fun replace(input: CharSequence, transform: (MatchResult) -> String): String { var match = match(input) if (match == null) return input.toString() @@ -68,9 +109,19 @@ public class Regex(pattern: String, options: Set) { return sb.toString() } + /** + * Replaces the first occurrence of this regular expression in the specified [input] string with specified [replacement] expression. + * + * @param replacement A replacement expression that can include substitutions. See [Matcher.appendReplacement] for details. + */ public fun replaceFirst(input: CharSequence, replacement: String): String = input.toString().nativeReplace(RegExp(pattern, options.map { it.value }.joinToString()), replacement) + /** + * Splits this string around matches of the given regular expression. + * + * @param limit The maximum number of times the split can occur. + */ 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) } @@ -85,11 +136,15 @@ public class Regex(pattern: String, options: Set) { return result } + /** Returns the string representation of this regular expression. */ public override fun toString(): String = nativePattern.toString() companion object { + /** Returns a literal regex for the specified [literal] string. */ public fun fromLiteral(literal: String): Regex = Regex(escape(literal)) + /** Returns a literal pattern for the specified [literal] string. */ public fun escape(literal: String): String = literal.nativeReplace(patternEscape, "\\$&") + /** Returns a literal replacement exression for the specified [literal] string. */ public fun escapeReplacement(literal: String): String = literal.nativeReplace(replacementEscape, "$$$$") private val patternEscape = RegExp("""[-\\^$*+?.()|[\]{}]""", "g") @@ -97,6 +152,9 @@ public class Regex(pattern: String, options: Set) { } } +/** + * Creates a regular expression from the specified [pattern] string and the specified [options]. + */ public fun Regex(pattern: String, vararg options: RegexOption): Regex = Regex(pattern, options.toSet()) diff --git a/libraries/stdlib/src/kotlin/text/StringsJVM.kt b/libraries/stdlib/src/kotlin/text/StringsJVM.kt index 95e66d5df86..5d0f3aafb96 100644 --- a/libraries/stdlib/src/kotlin/text/StringsJVM.kt +++ b/libraries/stdlib/src/kotlin/text/StringsJVM.kt @@ -409,7 +409,7 @@ public fun CharSequence.slice(range: IntRange): CharSequence { /** * Converts the string into a regular expression [Pattern] optionally - * with the specified flags from [Pattern] or'd together + * with the specified [flags] from [Pattern] or'd together * so that strings can be split or matched on. */ public fun String.toPattern(flags: Int = 0): java.util.regex.Pattern { diff --git a/libraries/stdlib/src/kotlin/text/regex/Regex.kt b/libraries/stdlib/src/kotlin/text/regex/Regex.kt index 753dea9515b..8c534788024 100644 --- a/libraries/stdlib/src/kotlin/text/regex/Regex.kt +++ b/libraries/stdlib/src/kotlin/text/regex/Regex.kt @@ -16,17 +16,36 @@ package kotlin.text - +/** Represents a collection of captured groups in a single match. */ public trait MatchGroupCollection : Collection { + + /** Returns a group with the specified [index] + * + * @return An instance of [MatchGroup] if the group with the specified [index] was matched or `null` otherwise. + * + * The groups are indexed from 1 to the count of groups in regular expression. The group with zero index + * represents the entire match. + */ public fun get(index: Int): MatchGroup? + + // TODO: Provide get(name: String) on JVM 7+ } +/** + * Represents the results from a single regular expression match. + */ public trait MatchResult { + /** The range of indices in the original string where match was captured. */ public val range: IntRange + /** The substring from the input string captured by this match. */ public val value: String + /** A collection of groups matched by the regular expression. */ public val groups: MatchGroupCollection // TODO: Should we have groupCount (equals groups.size()-1)? + /** Returns a new [MatchResult] with the results for the next match, starting at the position + * at which the last match ended (at the character after the last matched character). + */ public fun next(): MatchResult? } diff --git a/libraries/stdlib/src/kotlin/text/regex/RegexExtensions.kt b/libraries/stdlib/src/kotlin/text/regex/RegexExtensions.kt index 4c93b932132..e5cb6f8ff51 100644 --- a/libraries/stdlib/src/kotlin/text/regex/RegexExtensions.kt +++ b/libraries/stdlib/src/kotlin/text/regex/RegexExtensions.kt @@ -1,6 +1,12 @@ package kotlin import kotlin.text.* - +/** + * Converts the string into a regular expression [Regex] with the specified [options]. + */ public fun String.toRegex(vararg options: RegexOption): Regex = Regex(this, *options) + +/** + * Converts the string into a regular expression [Regex] with the specified set of [options]. + */ public fun String.toRegex(options: Set): Regex = Regex(this, options) diff --git a/libraries/stdlib/src/kotlin/text/regex/RegexJVM.kt b/libraries/stdlib/src/kotlin/text/regex/RegexJVM.kt index 7927cc04a05..5b0e8c814ff 100644 --- a/libraries/stdlib/src/kotlin/text/regex/RegexJVM.kt +++ b/libraries/stdlib/src/kotlin/text/regex/RegexJVM.kt @@ -19,53 +19,121 @@ package kotlin.text import java.util.regex.Pattern import java.util.regex.Matcher -public trait FlagEnum { +private trait FlagEnum { public val value: Int public val mask: Int } -public fun Iterable.toInt(): Int = +private fun Iterable.toInt(): Int = this.fold(0, { value, option -> value or option.value }) -public fun fromInt(value: Int, allValues: Array): Set = +private fun fromInt(value: Int, allValues: Array): Set = allValues.filter({ value and it.mask == it.value }).toSet() - +/** + * Provides enumeration values to use to set regular expression options. + */ public enum class RegexOption(override val value: Int, override val mask: Int = value) : FlagEnum { // common + + /** Enables case-insensitive matching. Case comparison is Unicode-aware. */ IGNORE_CASE : RegexOption(Pattern.CASE_INSENSITIVE) + + /** Enables multiline mode. + * + * In multiline mode the expressions `^` and `$` match just after or just before, + * respectively, a line terminator or the end of the input sequence. */ MULTILINE : RegexOption(Pattern.MULTILINE) //jvm-specific + + /** Enables literal parsing of the pattern. + * + * Metacharacters or escape sequences in the input sequence will be given no special meaning. + */ LITERAL : RegexOption(Pattern.LITERAL) - UNICODE_CASE: RegexOption(Pattern.UNICODE_CASE) - UNIX_LINES: RegexOption(Pattern.UNIX_LINES) + +// // Unicode case is enabled by default with the IGNORE_CASE +// /** Enables Unicode-aware case folding. */ +// UNICODE_CASE: RegexOption(Pattern.UNICODE_CASE) + + /** Enables Unix lines mode. + * In this mode, only the `'\n'` is recognized as a line terminator. + */ + UNIX_LINES: RegexOption(Pattern.UNIX_LINES) // TODO: Remove this + + /** Permits whitespace and comments in pattern. */ COMMENTS: RegexOption(Pattern.COMMENTS) + + /** Enables the mode, when the expression `.` matches any character, + * including a line terminator. + */ DOT_MATCHES_ALL: RegexOption(Pattern.DOTALL) + + /** Enables equivalence by canonical decomposition. */ CANON_EQ: RegexOption(Pattern.CANON_EQ) } -public data class MatchGroup(val value: String, val range: IntRange) +/** + * Represents the results from a single capturing group within a [MatchResult] of [Regex]. + * + * @param value The value of captured group. + * @param range The range of indices in the input string where group was captured. + * + * The [range] property is available on JVM only + */ +public data class MatchGroup(public val value: String, public val range: IntRange) -public class Regex( /* visibility? */ val nativePattern: Pattern) { +/** + * Represents an immutable regular expression. + * + * For pattern syntax reference see [java.util.regex.Pattern] + */ +public class Regex( /* visibility? */ public val nativePattern: Pattern) { - public constructor(pattern: String, options: Set): this(Pattern.compile(pattern, options.toInt())) + /** Creates a regular expression from the specified [pattern] string and the specified set of [options]. */ + public constructor(pattern: String, options: Set): this(Pattern.compile(pattern, ensureUnicodeCase(options.toInt()))) + + /** Creates a regular expression from the specified [pattern] string and the specified [options]. */ public constructor(pattern: String, vararg options: RegexOption) : this(pattern, options.toSet()) + /** The pattern string of this regular expression. */ public val pattern: String get() = nativePattern.pattern() + /** The set of options that were used to create this regular expression. */ public val options: Set = fromInt(nativePattern.flags(), RegexOption.values()) + /** Indicates whether the regular expression matches the entire [input]. */ public fun matches(input: CharSequence): Boolean = nativePattern.matcher(input).matches() - public fun match(input: CharSequence): MatchResult? = nativePattern.matcher(input).findNext(0) + /** Indicates whether the regular expression can find at least a match in the specified [input]. */ + public fun hasMatch(input: CharSequence): Boolean = nativePattern.matcher(input).find() - public fun matchAll(input: CharSequence): Sequence = sequence({ match(input) }, { match -> match.next() }) + /** + * Returns the first match of a regular expression in the [input], beginning at the specified [startIndex]. + * + * @param startIndex An index to start search with, by default 0. Must be not less than zero and not greater than `input.length()` + * @return An instance of [MatchResult] if match was found or `null` otherwise. + */ + public fun match(input: CharSequence, startIndex: Int = 0): MatchResult? = nativePattern.matcher(input).findNext(startIndex) + /** + * Returns a sequence of all occurrences of a regular expression within the [input] string, beginning at the specified [startIndex]. + */ + public fun matchAll(input: CharSequence, startIndex: Int = 0): Sequence = sequence({ match(input, startIndex) }, { match -> match.next() }) + + /** + * Replaces all occurrences of this regular expression in the specified [input] string with specified [replacement] expression. + * + * @param replacement A replacement expression that can include substitutions. See [Matcher.appendReplacement] for details. + */ public fun replace(input: CharSequence, replacement: String): String = nativePattern.matcher(input).replaceAll(replacement) - public fun replaceFirst(input: CharSequence, replacement: String): String = nativePattern.matcher(input).replaceFirst(replacement) - + /** + * Replaces all occurrences of this regular expression in the specified [input] string with the result of + * the given function [transform] that takes [MatchResult] and returns a string to be used as a + * replacement for that match. + */ public inline fun replace(input: CharSequence, transform: (MatchResult) -> String): String { var match = match(input) if (match == null) return input.toString() @@ -88,21 +156,46 @@ public class Regex( /* visibility? */ val nativePattern: Pattern) { return sb.toString() } + /** + * Replaces the first occurrence of this regular expression in the specified [input] string with specified [replacement] expression. + * + * @param replacement A replacement expression that can include substitutions. See [Matcher.appendReplacement] for details. + */ + public fun replaceFirst(input: CharSequence, replacement: String): String = nativePattern.matcher(input).replaceFirst(replacement) + + + /** + * Splits this string around matches of the given regular expression. + * + * @param limit The maximum number of times the split can occur. + */ 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() } + /** Returns the string representation of this regular expression, namely the [pattern] of this regular expression. */ public override fun toString(): String = nativePattern.toString() companion object { + /** Returns a literal regex for the specified [literal] string. */ public fun fromLiteral(literal: String): Regex = Regex(literal, RegexOption.LITERAL) + /** Returns a literal pattern for the specified [literal] string. */ public fun escape(literal: String): String = Pattern.quote(literal) + /** Returns a literal replacement exression for the specified [literal] string. */ public fun escapeReplacement(literal: String): String = Matcher.quoteReplacement(literal) + + private fun ensureUnicodeCase(flags: Int) = if (flags and Pattern.CASE_INSENSITIVE != 0) flags or Pattern.UNICODE_CASE else flags } } +public fun Pattern.asRegex(): Regex = Regex(this) +// TODO: function as/toPattern + + +// implementation + private fun Matcher.findNext(from: Int): MatchResult? { if (!find(from)) return null