Skip to content

Commit 70cca3d

Browse files
committed
Merge pull request #199 from CodaFi/stringly-typed
Improve String Extension
2 parents 9668a27 + 33a3b78 commit 70cca3d

File tree

7 files changed

+294
-16
lines changed

7 files changed

+294
-16
lines changed

Cartfile.resolved

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
github "typelift/SwiftCheck" "v0.2.2"
1+
github "typelift/SwiftCheck" "v0.2.3"
22
github "typelift/Swiftx" "v0.2.1"

Swiftz/StringExt.swift

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@
66
// Copyright (c) 2014 Maxwell Swadling. All rights reserved.
77
//
88

9+
/// An enum representing the possible values a string can match against.
10+
public enum StringMatcher {
11+
/// The empty string.
12+
case Nil
13+
/// A cons cell.
14+
case Cons(Character, String)
15+
}
16+
917
extension String {
1018
/// Returns an array of strings at newlines.
1119
public func lines() -> [String] {
@@ -22,4 +30,180 @@ extension String {
2230
public static func lines() -> Iso<String, String, [String], [String]> {
2331
return Iso(get: { $0.lines() }, inject: unlines)
2432
}
33+
34+
/// Appends a character onto the front of a string.
35+
public static func cons(head : Character, tail : String) -> String {
36+
return String(head) + tail
37+
}
38+
39+
/// Creates a string of n repeating characters.
40+
public static func replicate(n : UInt, value : Character) -> String {
41+
var l = ""
42+
for _ in 0..<n {
43+
l = String.cons(value, tail: l)
44+
}
45+
return l
46+
}
47+
48+
/// Destructures a string. If the string is empty the result is .Nil, otherwise the result is
49+
/// .Cons(head, tail).
50+
public func match() -> StringMatcher {
51+
if count(self) == 0 {
52+
return .Nil
53+
} else if count(self) == 1 {
54+
return .Cons(self[self.startIndex], "")
55+
}
56+
return .Cons(self[self.startIndex], self[advance(self.startIndex, 1)..<self.endIndex])
57+
}
58+
59+
/// Returns a string containing the characters of the receiver in reverse order.
60+
public func reverse() -> String {
61+
return self.reduce(flip(String.cons), initial: "")
62+
}
63+
64+
/// Maps a function over the characters of a string and returns a new string of those values.
65+
public func map(f : Character -> Character) -> String {
66+
switch self.match() {
67+
case .Nil:
68+
return ""
69+
case let .Cons(hd, tl):
70+
return String(f(hd)) + tl.map(f)
71+
}
72+
}
73+
74+
/// Removes characters from the receiver that do not satisfy a given predicate.
75+
public func filter(p : Character -> Bool) -> String {
76+
switch self.match() {
77+
case .Nil:
78+
return ""
79+
case let .Cons(x, xs):
80+
return p(x) ? (String(x) + xs.filter(p)) : xs.filter(p)
81+
}
82+
}
83+
84+
/// Applies a binary operator to reduce the characters of the receiver to a single value.
85+
public func reduce<B>(f : B -> Character -> B, initial : B) -> B {
86+
switch self.match() {
87+
case .Nil:
88+
return initial
89+
case let .Cons(x, xs):
90+
return xs.reduce(f, initial: f(initial)(x))
91+
}
92+
}
93+
94+
/// Applies a binary operator to reduce the characters of the receiver to a single value.
95+
public func reduce<B>(f : (B, Character) -> B, initial : B) -> B {
96+
switch self.match() {
97+
case .Nil:
98+
return initial
99+
case let .Cons(x, xs):
100+
return xs.reduce(f, initial: f(initial, x))
101+
}
102+
}
103+
104+
/// Takes two lists and returns true if the first string is a prefix of the second string.
105+
public func isPrefixOf(r : String) -> Bool {
106+
switch (self.match(), r.match()) {
107+
case (.Cons(let x, let xs), .Cons(let y, let ys)) where (x == y):
108+
return xs.isPrefixOf(ys)
109+
case (.Nil, _):
110+
return true
111+
default:
112+
return false
113+
}
114+
}
115+
116+
/// Takes two lists and returns true if the first string is a suffix of the second string.
117+
public func isSuffixOf(r : String) -> Bool {
118+
return self.reverse().isPrefixOf(r.reverse())
119+
}
120+
121+
/// Takes two lists and returns true if the first string is contained entirely anywhere in the
122+
/// second string.
123+
public func isInfixOf(r : String) -> Bool {
124+
func tails(l : String) -> [String] {
125+
return l.reduce({ x, y in
126+
return [String.cons(y, tail: head(x)!)] + x
127+
}, initial: [""])
128+
}
129+
130+
return any(tails(r), { self.isPrefixOf($0) })
131+
}
132+
133+
/// Takes two strings and drops items in the first from the second. If the first string is not a
134+
/// prefix of the second string this function returns Nothing.
135+
public func stripPrefix(r : String) -> Optional<String> {
136+
switch (self.match(), r.match()) {
137+
case (.Nil, _):
138+
return .Some(r)
139+
case (.Cons(let x, let xs), .Cons(let y, let ys)) where x == y:
140+
return xs.stripPrefix(xs)
141+
default:
142+
return .None
143+
}
144+
}
145+
146+
/// Takes two strings and drops items in the first from the end of the second. If the first
147+
/// string is not a suffix of the second string this function returns nothing.
148+
public func stripSuffix(r : String) -> Optional<String> {
149+
return self.reverse().stripPrefix(r.reverse()).map({ $0.reverse() })
150+
}
151+
}
152+
153+
extension String : Monoid {
154+
typealias M = String
155+
156+
public static var mzero : String {
157+
return ""
158+
}
159+
160+
public func op(other : String) -> String {
161+
return self + other
162+
}
163+
}
164+
165+
public func <>(l : String, r : String) -> String {
166+
return l + r
167+
}
168+
169+
extension String : Functor {
170+
typealias A = Character
171+
typealias B = Character
172+
typealias FB = String
173+
174+
public func fmap(f : Character -> Character) -> String {
175+
return self.map(f)
176+
}
177+
}
178+
179+
public func <^> (f : Character -> Character, l : String) -> String {
180+
return l.fmap(f)
181+
}
182+
183+
extension String : Pointed {
184+
public static func pure(x : Character) -> String {
185+
return String(x)
186+
}
187+
}
188+
189+
extension String : Applicative {
190+
typealias FAB = [Character -> Character]
191+
192+
public func ap(a : [Character -> Character]) -> String {
193+
return a.map({ return self.map($0) }).reduce("", combine: +)
194+
}
195+
}
196+
197+
public func <*> (f : Array<(Character -> Character)>, l : String) -> String {
198+
return l.ap(f)
199+
}
200+
201+
extension String : Monad {
202+
public func bind(f : Character -> String) -> String {
203+
return Array(self).map(f).reduce("", combine: +)
204+
}
205+
}
206+
207+
public func >>- (l : String, f : Character -> String) -> String {
208+
return l.bind(f)
25209
}

SwiftzTests/EitherSpec.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ struct EitherOf<A : Arbitrary, B : Arbitrary> : Arbitrary {
2626
}
2727

2828
static func arbitrary() -> Gen<EitherOf<A, B>> {
29-
return oneOf([ liftM(Either.left)(m1: A.arbitrary()), liftM(Either.right)(m1: B.arbitrary()) ]).fmap(EitherOf.create)
29+
return Gen.oneOf([ liftM(Either.left)(m1: A.arbitrary()), liftM(Either.right)(m1: B.arbitrary()) ]).fmap(EitherOf.create)
3030
}
3131

3232
static func shrink(bl : EitherOf<A, B>) -> [EitherOf<A, B>] {

SwiftzTests/ListSpec.swift

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ struct ListOf<A : Arbitrary> : Arbitrary, Printable {
2727
}
2828

2929
static func arbitrary() -> Gen<ListOf<A>> {
30-
return sized { n in
31-
return choose((0, n)).bind { k in
30+
return Gen.sized { n in
31+
return Gen<Int>.choose((0, n)).bind { k in
3232
if k == 0 {
3333
return Gen.pure(ListOf([]))
3434
}
@@ -39,12 +39,7 @@ struct ListOf<A : Arbitrary> : Arbitrary, Printable {
3939
}
4040

4141
static func shrink(bl : ListOf<A>) -> [ListOf<A>] {
42-
switch bl.getList.match() {
43-
case .Nil:
44-
return []
45-
case let .Cons(x, xs):
46-
return [ ListOf<A>(xs) ] + ListOf<A>.shrink(ListOf<A>(xs)) + ListOf<A>.shrink(ListOf<A>(List(fromArray: A.shrink(x)) + xs))
47-
}
42+
return ArrayOf.shrink(ArrayOf([A](bl.getList))).map({ ListOf(List(fromArray: $0.getArray)) })
4843
}
4944
}
5045

@@ -136,10 +131,6 @@ class ListSpec : XCTestCase {
136131
return xs.getList.map(+1) == xs.getList.fmap(+1)
137132
}
138133

139-
property["map behaves"] = forAll { (xs : ListOf<Int>) in
140-
return xs.getList.map(+1) == xs.getList.fmap(+1)
141-
}
142-
143134
property["map behaves"] = forAll { (xs : ListOf<Int>) in
144135
let fs = { List<Int>.replicate(2, value: $0) }
145136
return (xs.getList >>- fs) == xs.getList.map(fs).reduce(+, initial: List())

SwiftzTests/MaybeSpec.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ struct MaybeOf<A : Arbitrary> : Arbitrary, Printable {
2626
}
2727

2828
static func arbitrary() -> Gen<MaybeOf<A>> {
29-
return frequency([(1, Gen.pure(MaybeOf(Maybe<A>.none()))), (3, liftM({ MaybeOf(Maybe<A>.just($0)) })(m1: A.arbitrary()))])
29+
return Gen.frequency([
30+
(1, Gen.pure(MaybeOf(Maybe<A>.none()))),
31+
(3, liftM({ MaybeOf(Maybe<A>.just($0)) })(m1: A.arbitrary()))
32+
])
3033
}
3134

3235
static func shrink(bl : MaybeOf<A>) -> [MaybeOf<A>] {

SwiftzTests/StringExtSpec.swift

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,105 @@ class StringExtSpec : XCTestCase {
1717
let u = String.unlines(l)
1818
return u == (x + "\n")
1919
}
20+
21+
property["Strings of Equatable elements obey reflexivity"] = forAll { (l : String) in
22+
return l == l
23+
}
24+
25+
property["Strings of Equatable elements obey symmetry"] = forAll { (x : String, y : String) in
26+
return (x == y) == (y == x)
27+
}
28+
29+
property["Strings of Equatable elements obey transitivity"] = forAll { (x : String, y : String, z : String) in
30+
if (x == y) && (y == z) {
31+
return x == z
32+
}
33+
return Discard()
34+
}
35+
36+
property["Strings of Equatable elements obey negation"] = forAll { (x : String, y : String) in
37+
return (x != y) == !(x == y)
38+
}
39+
40+
property["Strings of Comparable elements obey reflexivity"] = forAll { (l : String) in
41+
return l == l
42+
}
43+
44+
property["String obeys the Functor identity law"] = forAll { (x : String) in
45+
return (x.fmap(identity)) == identity(x)
46+
}
47+
48+
reportProperty["String obeys the Functor composition law"] = forAll { (f : ArrowOf<Character, Character>, g : ArrowOf<Character, Character>, x : String) in
49+
return ((f.getArrow g.getArrow) <^> x) == (x.fmap(f.getArrow).fmap(g.getArrow))
50+
}
51+
52+
property["String obeys the Applicative identity law"] = forAll { (x : String) in
53+
return (pure(identity) <*> x) == x
54+
}
55+
56+
reportProperty["String obeys the first Applicative composition law"] = forAll { (fl : ArrayOf<ArrowOf<Character, Character>>, gl : ArrayOf<ArrowOf<Character, Character>>, x : String) in
57+
let f = fl.getArray.map({ $0.getArrow })
58+
let g = gl.getArray.map({ $0.getArrow })
59+
return (curry() <^> f <*> g <*> x) == (f <*> (g <*> x))
60+
}
61+
62+
reportProperty["String obeys the second Applicative composition law"] = forAll { (fl : ArrayOf<ArrowOf<Character, Character>>, gl : ArrayOf<ArrowOf<Character, Character>>, x : String) in
63+
let f = fl.getArray.map({ $0.getArrow })
64+
let g = gl.getArray.map({ $0.getArrow })
65+
return (pure(curry()) <*> f <*> g <*> x) == (f <*> (g <*> x))
66+
}
67+
68+
property["String obeys the Monoidal left identity law"] = forAll { (x : String) in
69+
return (x + String.mzero) == x
70+
}
71+
72+
property["String obeys the Monoidal right identity law"] = forAll { (x : String) in
73+
return (String.mzero + x) == x
74+
}
75+
76+
property["cons behaves"] = forAll { (c : Character, s : String) in
77+
return String.cons(c, tail: s) == String(c) + s
78+
}
79+
80+
property["replicate behaves"] = forAll { (n : UInt, x : Character) in
81+
return String.replicate(n, value: x) == List.replicate(n, value: String(x)).reduce({ $0 + $1 }, initial: "")
82+
}
83+
84+
property["map behaves"] = forAll { (xs : String) in
85+
return xs.map(identity) == xs.fmap(identity)
86+
}
87+
88+
property["map behaves"] = forAll { (xs : String) in
89+
let fs : Character -> String = { String.replicate(2, value: $0) }
90+
return (xs >>- fs) == Array(xs).map(fs).reduce("", combine: +)
91+
}
92+
93+
reportProperty["filter behaves"] = forAll { (xs : String, pred : ArrowOf<Character, Bool>) in
94+
return xs.filter(pred.getArrow).reduce({ $0.0 && pred.getArrow($0.1) }, initial: true)
95+
}
96+
97+
property["isPrefixOf behaves"] = forAll { (s1 : String, s2 : String) in
98+
if s1.isPrefixOf(s2) {
99+
return s1.stripPrefix(s2) != nil
100+
}
101+
102+
if s2.isPrefixOf(s1) {
103+
return s2.stripPrefix(s1) != nil
104+
}
105+
106+
return Discard()
107+
}
108+
109+
property["isSuffixOf behaves"] = forAll { (s1 : String, s2 : String) in
110+
if s1.isSuffixOf(s2) {
111+
return s1.stripSuffix(s2) != nil
112+
}
113+
114+
if s2.isSuffixOf(s1) {
115+
return s2.stripSuffix(s1) != nil
116+
}
117+
118+
return Discard()
119+
}
20120
}
21121
}

0 commit comments

Comments
 (0)