From b9e07eb645a17404a84868886a7dced4918bbb80 Mon Sep 17 00:00:00 2001 From: Yuri Samsoniuk Date: Mon, 14 Jan 2013 21:44:17 +0200 Subject: [PATCH] [KT-1859] Added collection like extension methods to String. --- libraries/stdlib/src/kotlin/Strings.kt | 302 +++++++++++++++++++++++++ libraries/stdlib/test/StringTest.kt | 165 +++++++++++++- 2 files changed, 466 insertions(+), 1 deletion(-) diff --git a/libraries/stdlib/src/kotlin/Strings.kt b/libraries/stdlib/src/kotlin/Strings.kt index bf8625f272f..65271d0bce7 100644 --- a/libraries/stdlib/src/kotlin/Strings.kt +++ b/libraries/stdlib/src/kotlin/Strings.kt @@ -1,6 +1,9 @@ package kotlin import java.util.ArrayList +import java.util.HashMap +import java.util.HashSet +import java.util.LinkedList /** Returns the string with leading and trailing text matching the given string removed */ public inline fun String.trim(text: String) : String = trimLeading(text).trimTrailing(text) @@ -63,3 +66,302 @@ public inline fun String.count(predicate: (Char) -> Boolean): Int { } return answer } + +/** + * Filters characters which match the given predicate into new String object + * + * @includeFunctionBody ../../test/StringTest.kt filter + */ +public inline fun String.filter(predicate: (Char) -> Boolean): String = filterTo(StringBuilder(), predicate).toString() + +/** + * Returns an Appendable containing all characters which match the given *predicate* + * + * @includeFunctionBody ../../test/StringTest.kt filter + */ +public inline fun String.filterTo(result: T, predicate: (Char) -> Boolean): T +{ + for (с in this) if (predicate(с)) result.append(с) + return result +} + +/** + * Filters characters which match the given predicate into new String object + * + * @includeFunctionBody ../../test/StringTest.kt filterNot + */ +public inline fun String.filterNot(predicate: (Char) -> Boolean): String = filterNotTo(StringBuilder(), predicate).toString() + +/** + * Returns an Appendable containing all characters which do not match the given *predicate* + * + * @includeFunctionBody ../../test/StringTest.kt filterNot + */ +public inline fun String.filterNotTo(result: T, predicate: (Char) -> Boolean): T { + for (element in this) if (!predicate(element)) result.append(element) + return result +} + +/** + * Returns order of characters into a string + * + * @includeFunctionBody ../../test/StringTest.kt reverse + */ +public inline fun String.reverse(): String = StringBuilder(this).reverse().toString() + +/** + * Performs the given *operation* on each character + * + * @includeFunctionBody ../../test/StringTest.kt forEach + */ +public inline fun String.forEach(operation: (Char) -> Unit): Unit = for(c in this) operation(c) + +/** + * Returns *true* if all characters match the given *predicate* + * + * @includeFunctionBody ../../test/StringTest.kt all + */ +public inline fun String.all(predicate: (Char) -> Boolean): Boolean { + for(c in this) if(!predicate(c)) return false + return true +} + +/** + * Returns *true* if any character matches the given *predicate* + * + * @includeFunctionBody ../../test/StringTest.kt any + */ +public inline fun String.any(predicate: (Char) -> Boolean): Boolean { + for (c in this) if (predicate(c)) return true + return false +} + +/** + * Appends the string from all the characters separated using the *separator* and using the given *prefix* and *postfix* if supplied + * + * If a string could be huge you can specify a non-negative value of *limit* which will only show substring then it will + * a special *truncated* separator (which defaults to "..." + * + * @includeFunctionBody ../../test/StringTest.kt appendString + */ +public inline fun String.appendString(buffer: Appendable, separator: String = ", ", prefix: String = "", postfix: String = "", limit: Int = -1, truncated: String = "..."): Unit { + buffer.append(prefix) + var count = 0 + for (c in this) { + if (++count > 1) buffer.append(separator) + if (limit < 0 || count <= limit) buffer.append(c) else break + } + if (limit >= 0 && count > limit) buffer.append(truncated) + buffer.append(postfix) +} + +/** + * Returns the first character which matches the given *predicate* or *null* if none matched + * + * @includeFunctionBody ../../test/StringTest.kt find + */ +public inline fun String.find(predicate: (Char) -> Boolean): Char? { + for (c in this) if (predicate(c)) return c + return null +} + +/** + * Returns the first character which does not match the given *predicate* or *null* if none matched + * + * @includeFunctionBody ../../test/StringTest.kt findNot + */ +public inline fun String.findNot(predicate: (Char) -> Boolean): Char? { + for (c in this) if (!predicate(c)) return c + return null +} + +/** +* Partitions this string into a pair of string +* +* @includeFunctionBody ../../test/StringTest.kt partition +*/ +public inline fun String.partition(predicate: (Char) -> Boolean): Pair { + val first = StringBuilder() + val second = StringBuilder() + for (c in this) { + if (predicate(c)) { + first.append(c) + } else { + second.append(c) + } + } + return Pair(first.toString(), second.toString()) +} + +/** + * Returns the result of transforming each character to one or more values which are concatenated together into a single list + * + * @includeFunctionBody ../../test/StringTest.kt flatMap + */ +public inline fun String.flatMap(transform: (Char) -> Collection): Collection = flatMapTo(ArrayList(), transform) + +/** + * Returns the result of transforming each character to one or more values which are concatenated together into a passed list + * + * @includeFunctionBody ../../test/StringTest.kt flatMap + */ +public inline fun String.flatMapTo(result: MutableCollection, transform: (Char) -> Collection): Collection { + for (c in this) result.addAll(transform(c)) + return result +} + +/** + * Folds all characters from left to right with the *initial* value to perform the operation on sequential pairs of characters + * + * @includeFunctionBody ../../test/StringTest.kt fold + */ +public inline fun String.fold(initial: R, operation: (R, Char) -> R): R { + var answer = initial + for (c in this) answer = operation(answer, c) + return answer +} + +/** + * Folds all characters from right to left with the *initial* value to perform the operation on sequential pairs of characters + * + * @includeFunctionBody ../../test/StringTest.kt foldRight + */ +public inline fun String.foldRight(initial: R, operation: (Char, R) -> R): R = reverse().fold(initial, { x, y -> operation(y, x) }) + +/** + * Applies binary operation to all characters in a string, going from left to right. + * Similar to fold function, but uses the first character as initial value + * + * @includeFunctionBody ../../test/StringTest.kt reduce + */ +public inline fun String.reduce(operation: (Char, Char) -> Char): Char { + val iterator = this.iterator() + if (!iterator.hasNext()) { + throw UnsupportedOperationException("Empty string can't be reduced") + } + + var result = iterator.next() + while (iterator.hasNext()) { + result = operation(result, iterator.next()) + } + + return result +} + +/** + * Applies binary operation to all characters in a string, going from right to left. + * Similar to foldRight function, but uses the last character as initial value + * + * @includeFunctionBody ../../test/StringTest.kt reduceRight + */ +public inline fun String.reduceRight(operation: (Char, Char) -> Char): Char = reverse().reduce { x, y -> operation(y, x) } + + +/** + * Groups the characters in the string into a new [[Map]] using the supplied *toKey* function to calculate the key to group the characters by + * + * @includeFunctionBody ../../test/StringTest.kt groupBy + */ +public inline fun String.groupBy(toKey: (Char) -> K): Map = groupByTo(HashMap(), toKey) + +/** + * Groups the characters in the string into the given [[Map]] using the supplied *toKey* function to calculate the key to group the characters by + * + * @includeFunctionBody ../../test/StringTest.kt groupBy + */ +public inline fun String.groupByTo(result: MutableMap, toKey: (Char) -> K): Map { + for (c in this) { + val key = toKey(c) + val str = result.getOrElse(key) { "" } + result[key] = str + c + } + return result +} + +/** + * Creates a new string from all the characters separated using the *separator* and using the given *prefix* and *postfix* if supplied. + * + * If a string could be huge you can specify a non-negative value of *limit* which will only show a substring then it will + * a special *truncated* separator (which defaults to "..." + * + * @includeFunctionBody ../../test/StringTest.kt makeString + */ +public inline fun String.makeString(separator: String = ", ", prefix: String = "", postfix: String = "", limit: Int = -1, truncated: String = "..."): String { + val buffer = StringBuilder() + appendString(buffer, separator, prefix, postfix, limit, truncated) + return buffer.toString() +} + +/** + * Returns an Appendable containing the everything but the first characters that satisfy the given *predicate* + * + * @includeFunctionBody ../../test/StringTest.kt dropWhile + */ +public inline fun String.dropWhileTo(result: T, predicate: (Char) -> Boolean): T { + var start = true + for (element in this) { + if (start && predicate(element)) { + // ignore + } else { + start = false + result.append(element) + } + } + return result +} + +/** + * Returns a new String containing the everything but the first characters that satisfy the given *predicate* + * + * @includeFunctionBody ../../test/StringTest.kt dropWhile + */ +public inline fun String.dropWhile(predicate: (Char) -> Boolean): String = dropWhileTo(StringBuilder(), predicate).toString() + +/** + * Returns a string containing everything but the first *n* characters + * + * @includeFunctionBody ../../test/StringTest.kt drop + */ +public inline fun String.drop(n: Int): String = dropWhile(countTo(n)) + +/** + * Returns an Appendable containing the first characters that satisfy the given *predicate* + * + * @includeFunctionBody ../../test/StringTest.kt takeWhile + */ +public inline fun String.takeWhileTo(result: T, predicate: (Char) -> Boolean): T { + for (c in this) if (predicate(c)) result.append(c) else break + return result +} + +/** + * Returns a new String containing the first characters that satisfy the given *predicate* + * + * @includeFunctionBody ../../test/StringTest.kt takeWhile + */ +public inline fun String.takeWhile(predicate: (Char) -> Boolean): String = takeWhileTo(StringBuilder(), predicate).toString() + +/** + * Returns a string containing the first *n* characters + * + * @includeFunctionBody ../../test/StringTest.kt take + */ +public inline fun String.take(n: Int): String = takeWhile(countTo(n)) + +/** Copies all characters into the given collection */ +public inline fun > String.toCollection(result: C): C { + for (c in this) result.add(c) + return result +} + +/** Copies all characters into a [[LinkedList]] */ +public inline fun String.toLinkedList(): LinkedList = toCollection(LinkedList()) + +/** Copies all characters into a [[List]] */ +public inline fun String.toList(): List = toCollection(ArrayList(this.length())) + +/** Copies all characters into a [[Collection] */ +public inline fun String.toCollection(): Collection = toCollection(ArrayList(this.length())) + +/** Copies all characters into a [[Set]] */ +public inline fun String.toSet(): Set = toCollection(HashSet()) diff --git a/libraries/stdlib/test/StringTest.kt b/libraries/stdlib/test/StringTest.kt index 30e8a1ededc..b57e15eabce 100644 --- a/libraries/stdlib/test/StringTest.kt +++ b/libraries/stdlib/test/StringTest.kt @@ -1,7 +1,7 @@ package test +import java.util.Collections import kotlin.test.* - import org.junit.Test as test class StringTest { @@ -49,4 +49,167 @@ class StringTest { assertEquals("uRL", "URL".decapitalize()) } + test fun filter() { + assertEquals("acdca", "abcdcba".filter { !it.equals('b') }) + assertEquals("1234", "a1b2c3d4".filter { it.isDigit() }) + } + + test fun filterNot() { + assertEquals("acdca", "abcdcba".filterNot { it.equals('b') }) + assertEquals("abcd", "a1b2c3d4".filterNot { it.isDigit() }) + } + + test fun reverse() { + assertEquals("dcba", "abcd".reverse()) + assertEquals("4321", "1234".reverse()) + assertEquals("", "".reverse()) + } + + test fun forEach() { + val data = "abcd1234" + var count = 0 + data.forEach{ count++ } + assertEquals(data.length(), count) + } + + test fun all() { + val data = "AbCd" + assertTrue { + data.all { it.isJavaLetter() } + } + assertNot { + data.all { it.isUpperCase() } + } + } + + test fun any() { + val data = "a1bc" + assertTrue { + data.any() { it.isDigit() } + } + assertNot { + data.any() { it.isUpperCase() } + } + } + + test fun appendString() { + val data = "kotlin" + val sb = StringBuilder() + data.appendString(sb, "^", "<", ">") + assertEquals("", sb.toString()) + } + + test fun find() { + val data = "a1b2c3" + assertEquals('1', data.find { it.isDigit() }) + assertNull(data.find { it.isUpperCase() }) + } + + test fun findNot() { + val data = "1a2b3c" + assertEquals('a', data.findNot { it.isDigit() }) + assertNull(data.findNot { it.isJavaLetterOrDigit() }) + } + + test fun partition() { + val data = "a1b2c3" + val pair = data.partition { it.isDigit() } + assertEquals("123", pair.first, "pair.first") + assertEquals("abc", pair.second, "pair.second") + } + + test fun flatMap() { + val data = "abcd" + val result = data.flatMap { Collections.singletonList(it) } + assertEquals(data.size, result.count()) + assertEquals(data.toCharList(), result) + } + + test fun fold() { + // calculate number of digits in the string + val data = "a1b2c3def" + val result = data.fold(0, { digits, c -> if(c.isDigit()) digits + 1 else digits } ) + assertEquals(3, result) + + //simulate all method + assertEquals(true, "ABCD".fold(true, { r, c -> r && c.isUpperCase() })) + + //get string back + assertEquals(data, data.fold("", { s, c -> s + c })) + } + + test fun foldRight() { + // calculate number of digits in the string + val data = "a1b2c3def" + val result = data.foldRight(0, { c, digits -> if(c.isDigit()) digits + 1 else digits }) + assertEquals(3, result) + + //simulate all method + assertEquals(true, "ABCD".foldRight(true, { c, r -> r && c.isUpperCase() })) + + //get string back + assertEquals(data, data.foldRight("", { s, c -> "" + s + c })) + } + + test fun reduce() { + // get the smallest character(by char value) + assertEquals('a', "bacfd".reduce { v, c -> if (v > c) c else v }) + + failsWith { + "".reduce { a, b -> '\n' } + } + } + + test fun reduceRight() { + // get the smallest character(by char value) + assertEquals('a', "bacfd".reduceRight { c, v -> if (v > c) c else v }) + + failsWith { + "".reduceRight { a, b -> '\n' } + } + } + + test fun groupBy() { + // collect similar characters by their int code + val data = "ababaaabcd" + val result = data.groupBy { it.toInt() } + assertEquals(4, result.size) + assertEquals("bbb", result.get('b'.toInt())) + } + + test fun makeString() { + val data = "abcd" + val result = data.makeString("_", "(", ")") + assertEquals("(a_b_c_d)", result) + + val data2 = "verylongstring" + val result2 = data2.makeString("-", "[", "]", 11, "oops") + assertEquals("[v-e-r-y-l-o-n-g-s-t-r-oops]", result2) + } + + test fun dropWhile() { + val data = "ab1cd2" + val result = data.dropWhile { it.isJavaLetter() } + assertEquals("1cd2", result) + } + + test fun drop() { + val data = "abcd1234" + assertEquals("d1234", data.drop(3)) + assertEquals(data, data.drop(-2)) + assertEquals("", data.drop(data.length + 5)) + } + + test fun takeWhile() { + val data = "ab1cd2" + val result = data.takeWhile { it.isJavaLetter() } + assertEquals("ab", result) + } + + test fun take() { + val data = "abcd1234" + assertEquals("abc", data.take(3)) + assertEquals("", data.take(-7)) + assertEquals(data, data.take(data.length + 42)) + } }