Skip to content

Commit 22780e8

Browse files
committed
Add StringOps
Fixes #554
1 parent 06526ea commit 22780e8

File tree

6 files changed

+759
-31
lines changed

6 files changed

+759
-31
lines changed

compat/src/main/scala-2.11_2.12/scala/collection/compat/PackageShared.scala

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,60 @@ private[compat] trait PackageShared {
5353
def newBuilder: m.Builder[A, C] = factory()
5454
}
5555

56+
implicit class StringOps(s: String) {
57+
58+
/**
59+
* Try to parse as a `Boolean`
60+
* @return `Some(true)` if the string is "true" case insensitive,
61+
* `Some(false)` if the string is "false" case insensitive,
62+
* and `None` if the string is anything else
63+
* @throws java.lang.NullPointerException if the string is `null`
64+
*/
65+
def toBooleanOption: Option[Boolean] = StringParsers.parseBool(s)
66+
67+
/**
68+
* Try to parse as a `Byte`
69+
* @return `Some(value)` if the string contains a valid byte value, otherwise `None`
70+
* @throws java.lang.NullPointerException if the string is `null`
71+
*/
72+
def toByteOption: Option[Byte] = StringParsers.parseByte(s)
73+
74+
/**
75+
* Try to parse as a `Short`
76+
* @return `Some(value)` if the string contains a valid short value, otherwise `None`
77+
* @throws java.lang.NullPointerException if the string is `null`
78+
*/
79+
def toShortOption: Option[Short] = StringParsers.parseShort(s)
80+
81+
/**
82+
* Try to parse as an `Int`
83+
* @return `Some(value)` if the string contains a valid Int value, otherwise `None`
84+
* @throws java.lang.NullPointerException if the string is `null`
85+
*/
86+
def toIntOption: Option[Int] = StringParsers.parseInt(s)
87+
88+
/**
89+
* Try to parse as a `Long`
90+
* @return `Some(value)` if the string contains a valid long value, otherwise `None`
91+
* @throws java.lang.NullPointerException if the string is `null`
92+
*/
93+
def toLongOption: Option[Long] = StringParsers.parseLong(s)
94+
95+
/**
96+
* Try to parse as a `Float`
97+
* @return `Some(value)` if the string is a parsable `Float`, `None` otherwise
98+
* @throws java.lang.NullPointerException If the string is null
99+
*/
100+
def toFloatOption: Option[Float] = StringParsers.parseFloat(s)
101+
102+
/**
103+
* Try to parse as a `Double`
104+
* @return `Some(value)` if the string is a parsable `Double`, `None` otherwise
105+
* @throws java.lang.NullPointerException If the string is null
106+
*/
107+
def toDoubleOption: Option[Double] = StringParsers.parseDouble(s)
108+
}
109+
56110
implicit def genericCompanionToCBF[A, CC[X] <: GenTraversable[X]](
57111
fact: GenericCompanion[CC]): CanBuildFrom[Any, A, CC[A]] = {
58112
/* see https://github.com/scala/scala-collection-compat/issues/337
Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
/*
2+
* Scala (https://www.scala-lang.org)
3+
*
4+
* Copyright EPFL and Lightbend, Inc.
5+
*
6+
* Licensed under Apache License 2.0
7+
* (http://www.apache.org/licenses/LICENSE-2.0).
8+
*
9+
* See the NOTICE file distributed with this work for
10+
* additional information regarding copyright ownership.
11+
*/
12+
13+
package scala
14+
package collection
15+
package compat
16+
17+
import scala.annotation.tailrec
18+
19+
/** A module containing the implementations of parsers from strings to numeric types, and boolean
20+
*/
21+
private[scala] object StringParsers {
22+
23+
//compile-time constant helpers
24+
25+
//Int.MinValue == -2147483648
26+
private final val intOverflowBoundary = -214748364
27+
private final val intOverflowDigit = 9
28+
//Long.MinValue == -9223372036854775808L
29+
private final val longOverflowBoundary = -922337203685477580L
30+
private final val longOverflowDigit = 9
31+
32+
@inline
33+
private[this] final def decValue(ch: Char): Int = java.lang.Character.digit(ch, 10)
34+
35+
@inline
36+
private[this] final def stepToOverflow(from: String,
37+
len: Int,
38+
agg: Int,
39+
isPositive: Boolean,
40+
min: Int): Option[Int] = {
41+
@tailrec
42+
def rec(i: Int, agg: Int): Option[Int] =
43+
if (agg < min) None
44+
else if (i == len) {
45+
if (!isPositive) Some(agg)
46+
else if (agg == min) None
47+
else Some(-agg)
48+
} else {
49+
val digit = decValue(from.charAt(i))
50+
if (digit == -1) None
51+
else rec(i + 1, agg * 10 - digit)
52+
}
53+
rec(1, agg)
54+
}
55+
56+
@inline
57+
private[this] final def isDigit(c: Char): Boolean = c >= '0' && c <= '9'
58+
59+
//bool
60+
@inline
61+
final def parseBool(from: String): Option[Boolean] =
62+
if (from.equalsIgnoreCase("true")) Some(true)
63+
else if (from.equalsIgnoreCase("false")) Some(false)
64+
else None
65+
66+
//integral types
67+
final def parseByte(from: String): Option[Byte] = {
68+
val len = from.length()
69+
//empty strings parse to None
70+
if (len == 0) None
71+
else {
72+
val first = from.charAt(0)
73+
val v = decValue(first)
74+
if (len == 1) {
75+
//"+" and "-" parse to None
76+
if (v > -1) Some(v.toByte)
77+
else None
78+
} else if (v > -1) stepToOverflow(from, len, -v, true, Byte.MinValue).map(_.toByte)
79+
else if (first == '+') stepToOverflow(from, len, 0, true, Byte.MinValue).map(_.toByte)
80+
else if (first == '-') stepToOverflow(from, len, 0, false, Byte.MinValue).map(_.toByte)
81+
else None
82+
}
83+
}
84+
85+
final def parseShort(from: String): Option[Short] = {
86+
val len = from.length()
87+
//empty strings parse to None
88+
if (len == 0) None
89+
else {
90+
val first = from.charAt(0)
91+
val v = decValue(first)
92+
if (len == 1) {
93+
//"+" and "-" parse to None
94+
if (v > -1) Some(v.toShort)
95+
else None
96+
} else if (v > -1) stepToOverflow(from, len, -v, true, Short.MinValue).map(_.toShort)
97+
else if (first == '+') stepToOverflow(from, len, 0, true, Short.MinValue).map(_.toShort)
98+
else if (first == '-') stepToOverflow(from, len, 0, false, Short.MinValue).map(_.toShort)
99+
else None
100+
}
101+
}
102+
103+
final def parseInt(from: String): Option[Int] = {
104+
val len = from.length()
105+
106+
@tailrec
107+
def step(i: Int, agg: Int, isPositive: Boolean): Option[Int] = {
108+
if (i == len) {
109+
if (!isPositive) Some(agg)
110+
else if (agg == Int.MinValue) None
111+
else Some(-agg)
112+
} else if (agg < intOverflowBoundary) None
113+
else {
114+
val digit = decValue(from.charAt(i))
115+
if (digit == -1 || (agg == intOverflowBoundary && digit == intOverflowDigit)) None
116+
else step(i + 1, (agg * 10) - digit, isPositive)
117+
}
118+
}
119+
//empty strings parse to None
120+
if (len == 0) None
121+
else {
122+
val first = from.charAt(0)
123+
val v = decValue(first)
124+
if (len == 1) {
125+
//"+" and "-" parse to None
126+
if (v > -1) Some(v)
127+
else None
128+
} else if (v > -1) step(1, -v, true)
129+
else if (first == '+') step(1, 0, true)
130+
else if (first == '-') step(1, 0, false)
131+
else None
132+
}
133+
}
134+
135+
final def parseLong(from: String): Option[Long] = {
136+
//like parseInt, but Longer
137+
val len = from.length()
138+
139+
@tailrec
140+
def step(i: Int, agg: Long, isPositive: Boolean): Option[Long] = {
141+
if (i == len) {
142+
if (isPositive && agg == Long.MinValue) None
143+
else if (isPositive) Some(-agg)
144+
else Some(agg)
145+
} else if (agg < longOverflowBoundary) None
146+
else {
147+
val digit = decValue(from.charAt(i))
148+
if (digit == -1 || (agg == longOverflowBoundary && digit == longOverflowDigit)) None
149+
else step(i + 1, agg * 10 - digit, isPositive)
150+
}
151+
}
152+
//empty strings parse to None
153+
if (len == 0) None
154+
else {
155+
val first = from.charAt(0)
156+
val v = decValue(first).toLong
157+
if (len == 1) {
158+
//"+" and "-" parse to None
159+
if (v > -1) Some(v)
160+
else None
161+
} else if (v > -1) step(1, -v, true)
162+
else if (first == '+') step(1, 0, true)
163+
else if (first == '-') step(1, 0, false)
164+
else None
165+
}
166+
}
167+
168+
//floating point
169+
final def checkFloatFormat(format: String): Boolean = {
170+
//indices are tracked with a start index which points *at* the first index
171+
//and an end index which points *after* the last index
172+
//so that slice length === end - start
173+
//thus start == end <=> empty slice
174+
//and format.substring(start, end) is equivalent to the slice
175+
176+
//some utilities for working with index bounds into the original string
177+
@inline
178+
def forAllBetween(start: Int, end: Int, pred: Char => Boolean): Boolean = {
179+
@tailrec
180+
def rec(i: Int): Boolean = i >= end || pred(format.charAt(i)) && rec(i + 1)
181+
rec(start)
182+
}
183+
184+
//one after last index for the predicate to hold, or `from` if none hold
185+
//may point after the end of the string
186+
@inline
187+
def skipIndexWhile(predicate: Char => Boolean, from: Int, until: Int): Int = {
188+
@tailrec @inline
189+
def rec(i: Int): Int =
190+
if ((i < until) && predicate(format.charAt(i))) rec(i + 1)
191+
else i
192+
rec(from)
193+
}
194+
195+
def isHexFloatLiteral(startIndex: Int, endIndex: Int): Boolean = {
196+
def isHexDigit(ch: Char) =
197+
((ch >= '0' && ch <= '9') ||
198+
(ch >= 'a' && ch <= 'f') ||
199+
(ch >= 'A' && ch <= 'F'))
200+
201+
def prefixOK(startIndex: Int, endIndex: Int): Boolean = {
202+
val len = endIndex - startIndex
203+
(len > 0) && {
204+
//the prefix part is
205+
//hexDigits
206+
//hexDigits.
207+
//hexDigits.hexDigits
208+
//.hexDigits
209+
//but not .
210+
if (format.charAt(startIndex) == '.') {
211+
(len > 1) && forAllBetween(startIndex + 1, endIndex, isHexDigit)
212+
} else {
213+
val noLeading = skipIndexWhile(isHexDigit, startIndex, endIndex)
214+
(noLeading >= endIndex) ||
215+
((format.charAt(noLeading) == '.') && forAllBetween(noLeading + 1,
216+
endIndex,
217+
isHexDigit))
218+
}
219+
}
220+
}
221+
222+
def postfixOK(startIndex: Int, endIndex: Int): Boolean =
223+
(startIndex < endIndex) && {
224+
(forAllBetween(startIndex, endIndex, isDigit)) || {
225+
val startchar = format.charAt(startIndex)
226+
(startchar == '+' || startchar == '-') &&
227+
(endIndex - startIndex > 1) &&
228+
forAllBetween(startIndex + 1, endIndex, isDigit)
229+
}
230+
}
231+
// prefix [pP] postfix
232+
val pIndex = format.indexWhere(ch => ch == 'p' || ch == 'P', startIndex)
233+
(pIndex <= endIndex) && prefixOK(startIndex, pIndex) && postfixOK(pIndex + 1, endIndex)
234+
}
235+
236+
def isDecFloatLiteral(startIndex: Int, endIndex: Int): Boolean = {
237+
//invariant: endIndex > startIndex
238+
239+
def isExp(c: Char): Boolean = c == 'e' || c == 'E'
240+
241+
def expOK(startIndex: Int, endIndex: Int): Boolean =
242+
(startIndex < endIndex) && {
243+
val startChar = format.charAt(startIndex)
244+
if (startChar == '+' || startChar == '-')
245+
(endIndex > (startIndex + 1)) &&
246+
skipIndexWhile(isDigit, startIndex + 1, endIndex) == endIndex
247+
else skipIndexWhile(isDigit, startIndex, endIndex) == endIndex
248+
}
249+
250+
//significant can be one of
251+
//* digits.digits
252+
//* .digits
253+
//* digits.
254+
//but not just .
255+
val startChar = format.charAt(startIndex)
256+
if (startChar == '.') {
257+
val noSignificant = skipIndexWhile(isDigit, startIndex + 1, endIndex)
258+
// a digit is required followed by optional exp
259+
(noSignificant > startIndex + 1) && (noSignificant >= endIndex ||
260+
isExp(format.charAt(noSignificant)) && expOK(noSignificant + 1, endIndex))
261+
} else if (isDigit(startChar)) {
262+
// one set of digits, then optionally a period, then optionally another set of digits, then optionally an exponent
263+
val noInt = skipIndexWhile(isDigit, startIndex, endIndex)
264+
// just the digits
265+
(noInt == endIndex) || {
266+
if (format.charAt(noInt) == '.') {
267+
val noSignificant = skipIndexWhile(isDigit, noInt + 1, endIndex)
268+
(noSignificant >= endIndex) || //no exponent
269+
isExp(format.charAt(noSignificant)) && expOK(noSignificant + 1, endIndex)
270+
} else
271+
isExp(format.charAt(noInt)) && expOK(noInt + 1, endIndex)
272+
}
273+
} else false
274+
}
275+
276+
//count 0x00 to 0x20 as "whitespace", and nothing else
277+
val unspacedStart = format.indexWhere(ch => ch.toInt > 0x20)
278+
val unspacedEnd = format.lastIndexWhere(ch => ch.toInt > 0x20) + 1
279+
280+
if (unspacedStart == -1 || unspacedStart >= unspacedEnd || unspacedEnd <= 0) false
281+
else {
282+
//all formats can have a sign
283+
val unsigned = {
284+
val startchar = format.charAt(unspacedStart)
285+
if (startchar == '-' || startchar == '+') unspacedStart + 1 else unspacedStart
286+
}
287+
if (unsigned >= unspacedEnd) false
288+
//that's it for NaN and Infinity
289+
else if (format.charAt(unsigned) == 'N') format.substring(unsigned, unspacedEnd) == "NaN"
290+
else if (format.charAt(unsigned) == 'I') format.substring(unsigned, unspacedEnd) == "Infinity"
291+
else {
292+
//all other formats can have a format suffix
293+
val desuffixed = {
294+
val endchar = format.charAt(unspacedEnd - 1)
295+
if (endchar == 'f' || endchar == 'F' || endchar == 'd' || endchar == 'D') unspacedEnd - 1
296+
else unspacedEnd
297+
}
298+
val len = desuffixed - unsigned
299+
if (len <= 0) false
300+
else if (len >= 2 && (format.charAt(unsigned + 1) == 'x' || format.charAt(unsigned + 1) == 'X'))
301+
format.charAt(unsigned) == '0' && isHexFloatLiteral(unsigned + 2, desuffixed)
302+
else isDecFloatLiteral(unsigned, desuffixed)
303+
}
304+
}
305+
}
306+
307+
@inline
308+
def parseFloat(from: String): Option[Float] =
309+
if (checkFloatFormat(from)) Some(java.lang.Float.parseFloat(from))
310+
else None
311+
312+
@inline
313+
def parseDouble(from: String): Option[Double] =
314+
if (checkFloatFormat(from)) Some(java.lang.Double.parseDouble(from))
315+
else None
316+
317+
}

0 commit comments

Comments
 (0)