Background
Suppose you wish to find a partial text from a formatted phone number, and you wish to mark the finding.
For example, if you have this phone number: "+972 50-123-4567" , and you search for 2501 , you will be able to mark the text within it, of "2 50-1".
More examples of a hashmap of queries and the expected result, if the text to search in is "+972 50-123-45678", and the allowed characters are "01234567890+*#" :
val tests = hashMapOf(
"" to Pair(0, 0),
"9" to Pair(1, 2),
"97" to Pair(1, 3),
"250" to Pair(3, 7),
"250123" to Pair(3, 11),
"250118" to null,
"++" to null,
"8" to Pair(16, 17),
"+" to Pair(0, 1),
"+8" to null,
"78" to Pair(15, 17),
"5678" to Pair(13, 17),
"788" to null,
"+ " to Pair(0, 1),
" " to Pair(0, 0),
"+ 5" to null,
"+ 9" to Pair(0, 2)
)
The problem
You might think: Why not just use "indexOf" or clean the string and find the occurrence ?
But that's wrong, because I want to mark the occurrence, ignoring some characters on the way.
What I've tried
I actually have the answer after I worked on it for quite some time. Just wanted to share it, and optionally see if anyone can write a nicer/shorter code, that will produce the same behavior.
I had a solution before, which was quite shorter, but it assumed that the query contains only allowed characters.
The question
Well there is no question this time, because I've found an answer myself.
However, again, if you can think of a more elegant and/shorter solution, which is as efficient as what I wrote, please let me know.
I'm pretty sure regular expressions could be a solution here, but they tend to be unreadable sometimes, and also very inefficient compared to exact code. Still could also be nice to know how this kind of question would work for it. Maybe I could perform a small benchmark on it too.
OK so here's my solution, including a sample to test it:
TextSearchUtil.kt
object TextSearchUtil {
/**#return where the query was found. First integer is the start. The second is the last, excluding.
* Special cases: Pair(0,0) if query is empty or ignored, null if not found.
* #param text the text to search within. Only allowed characters are searched for. Rest are ignored
* #param query what to search for. Only allowed characters are searched for. Rest are ignored
* #param allowedCharactersSet the only characters we should be allowed to check. Rest are ignored*/
fun findOccurrenceWhileIgnoringCharacters(text: String, query: String, allowedCharactersSet: HashSet<Char>): Pair<Int, Int>? {
//get index of first char to search for
var searchIndexStart = -1
for ((index, c) in query.withIndex())
if (allowedCharactersSet.contains(c)) {
searchIndexStart = index
break
}
if (searchIndexStart == -1) {
//query contains only ignored characters, so it's like an empty one
return Pair(0, 0)
}
//got index of first character to search for
if (text.isEmpty())
//need to search for a character, but the text is empty, so not found
return null
var mainIndex = 0
while (mainIndex < text.length) {
var searchIndex = searchIndexStart
var isFirstCharToSearchFor = true
var secondaryIndex = mainIndex
var charToSearch = query[searchIndex]
secondaryLoop# while (secondaryIndex < text.length) {
//skip ignored characters on query
if (!isFirstCharToSearchFor)
while (!allowedCharactersSet.contains(charToSearch)) {
++searchIndex
if (searchIndex >= query.length) {
//reached end of search while all characters were fine, so found the match
return Pair(mainIndex, secondaryIndex)
}
charToSearch = query[searchIndex]
}
//skip ignored characters on text
var c: Char? = null
while (secondaryIndex < text.length) {
c = text[secondaryIndex]
if (allowedCharactersSet.contains(c))
break
else {
if (isFirstCharToSearchFor)
break#secondaryLoop
++secondaryIndex
}
}
//reached end of text
if (secondaryIndex == text.length) {
if (isFirstCharToSearchFor)
//couldn't find the first character anywhere, so failed to find the query
return null
break#secondaryLoop
}
//time to compare
if (c != charToSearch)
break#secondaryLoop
++searchIndex
isFirstCharToSearchFor = false
if (searchIndex >= query.length) {
//reached end of search while all characters were fine, so found the match
return Pair(mainIndex, secondaryIndex + 1)
}
charToSearch = query[searchIndex]
++secondaryIndex
}
++mainIndex
}
return null
}
}
Sample usage to test it :
MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//
val text = "+972 50-123-45678"
val allowedCharacters = "01234567890+*#"
val allowedPhoneCharactersSet = HashSet<Char>(allowedCharacters.length)
for (c in allowedCharacters)
allowedPhoneCharactersSet.add(c)
//
val tests = hashMapOf(
"" to Pair(0, 0),
"9" to Pair(1, 2),
"97" to Pair(1, 3),
"250" to Pair(3, 7),
"250123" to Pair(3, 11),
"250118" to null,
"++" to null,
"8" to Pair(16, 17),
"+" to Pair(0, 1),
"+8" to null,
"78" to Pair(15, 17),
"5678" to Pair(13, 17),
"788" to null,
"+ " to Pair(0, 1),
" " to Pair(0, 0),
"+ 5" to null,
"+ 9" to Pair(0, 2)
)
for (test in tests) {
val result = TextSearchUtil.findOccurrenceWhileIgnoringCharacters(text, test.key, allowedPhoneCharactersSet)
val isResultCorrect = result == test.value
val foundStr = if (result == null) null else text.substring(result.first, result.second)
when {
!isResultCorrect -> Log.e("AppLog", "checking query of \"${test.key}\" inside \"$text\" . Succeeded?$isResultCorrect Result: $result found String: \"$foundStr\"")
foundStr == null -> Log.d("AppLog", "checking query of \"${test.key}\" inside \"$text\" . Succeeded?$isResultCorrect Result: $result")
else -> Log.d("AppLog", "checking query of \"${test.key}\" inside \"$text\" . Succeeded?$isResultCorrect Result: $result found String: \"$foundStr\"")
}
}
//
Log.d("AppLog", "special cases:")
Log.d("AppLog", "${TextSearchUtil.findOccurrenceWhileIgnoringCharacters("a", "c", allowedPhoneCharactersSet) == Pair(0, 0)}")
Log.d("AppLog", "${TextSearchUtil.findOccurrenceWhileIgnoringCharacters("ab", "c", allowedPhoneCharactersSet) == Pair(0, 0)}")
Log.d("AppLog", "${TextSearchUtil.findOccurrenceWhileIgnoringCharacters("ab", "cd", allowedPhoneCharactersSet) == Pair(0, 0)}")
Log.d("AppLog", "${TextSearchUtil.findOccurrenceWhileIgnoringCharacters("a", "cd", allowedPhoneCharactersSet) == Pair(0, 0)}")
}
}
If I want to highlight the result, I can use something like that:
val pair = TextSearchUtil.findOccurrenceWhileIgnoringCharacters(text, "2501", allowedPhoneCharactersSet)
if (pair == null)
textView.text = text
else {
val wordToSpan = SpannableString(text)
wordToSpan.setSpan(BackgroundColorSpan(0xFFFFFF00.toInt()), pair.first, pair.second, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
textView.setText(wordToSpan, TextView.BufferType.SPANNABLE)
}
Related
I have the below Code which works, except for if there is a number in the text field so a single speech mark does not get added around say 1 but would be around one.
As an aside I don't want speechmarks on the first column (the ID value)
SEP = ", "
QUOTE = "\'"
NEWLINE = System.getProperty("line.separator")
KEYWORDS_LOWERCASE = com.intellij.database.util.DbSqlUtil.areKeywordsLowerCase(PROJECT)
KW_INSERT_INTO = KEYWORDS_LOWERCASE ? "insert into " : "INSERT INTO "
KW_VALUES = KEYWORDS_LOWERCASE ? ") values (" : ") VALUES ("
KW_NULL = KEYWORDS_LOWERCASE ? "null" : "NULL"
def record(columns, dataRow) {
OUT.append(KW_INSERT_INTO)
if (TABLE == null) OUT.append("MY_TABLE")
else OUT.append(TABLE.getParent().getName()).append(".").append(TABLE.getName())
OUT.append(" (")
columns.eachWithIndex { column, idx ->
OUT.append(column.name()).append(idx != columns.size() - 1 ? SEP : "")
}
OUT.append(KW_VALUES)
columns.eachWithIndex { column, idx ->
def value = dataRow.value(column)
def stringValue = value != null ? FORMATTER.format(dataRow, column) : KW_NULL
if (DIALECT.getDbms().isMysql())
stringValue = stringValue.replace("\\", "\\\\")
OUT.append(skipQuote ? "": QUOTE).append(stringValue.replace(QUOTE, QUOTE + QUOTE))
.append(skipQuote ? "": QUOTE).append(idx != columns.size() - 1 ? SEP : "")
}
OUT.append(");").append(NEWLINE)
}
ROWS.each { row -> record(COLUMNS, row) }
Not 100% sure what and why you are trying to achieve, but I would write down something like that in idiomatic groovy:
LinkedHashMap.metaClass.value = { delegate.get it } // mock value()
TABLE = [ parent:[ name:'OTHER_TABLE' ] ] // fake TABLE
KEYWORDS_LOWERCASE = true // false
KW_INSERT_INTO = 'INSERT INTO'
KW_VALUES = 'VALUES'
if( KEYWORDS_LOWERCASE ){
KW_INSERT_INTO = KW_INSERT_INTO.toLowerCase()
KW_VALUES = KW_VALUES.toLowerCase()
}
COLUMNS = [ 'a', 'nullllll', 'c', 'numberString' ]
String record(columns, dataRow) {
List values = columns.collect{
def v = dataRow.value it
switch( v ){
case Number:
case ~/\d+/: return v
case String: return "'$v'"
default: return 'null'
}
}
"$KW_INSERT_INTO ${TABLE?.parent?.name ?: 'MY_TABLE'} (${columns.join( ', ' )}) $KW_VALUES (${values.join( ', ' )});\n"
}
String res = record( COLUMNS, [ a:'aa', c:42, numberString:'84' ] )
assert res == "insert into OTHER_TABLE (a, nullllll, c, numberString) values ('aa', null, 42, 84);\n"
In switch statement the values are getting formatted.
You can try out yourself at https://groovyconsole.appspot.com/script/5151418931478528
The Spark Dataset.show() method is useful for seeing the contents of a dataset, particularly for debugging (it prints out a nicely-formatted table). As far as I can tell, it only prints to the console, but it would be useful to be able to get this as a string. For example, it would be nice to be able to write it to a log, or see it as the result of an expression when debugging with, say, IntelliJ.
Is there any way to get the output of Dataset.show() as a string?
The corresponding method behind show isn't visible from outside the sql package. I've taken the corresponding method and changed it such that a dataframe can be passed as parameter (code taken from Dataset.scala) :
def showString(df:DataFrame,_numRows: Int = 20, truncate: Int = 20): String = {
val numRows = _numRows.max(0)
val takeResult = df.take(numRows + 1)
val hasMoreData = takeResult.length > numRows
val data = takeResult.take(numRows)
// For array values, replace Seq and Array with square brackets
// For cells that are beyond `truncate` characters, replace it with the
// first `truncate-3` and "..."
val rows: Seq[Seq[String]] = df.schema.fieldNames.toSeq +: data.map { row =>
row.toSeq.map { cell =>
val str = cell match {
case null => "null"
case binary: Array[Byte] => binary.map("%02X".format(_)).mkString("[", " ", "]")
case array: Array[_] => array.mkString("[", ", ", "]")
case seq: Seq[_] => seq.mkString("[", ", ", "]")
case _ => cell.toString
}
if (truncate > 0 && str.length > truncate) {
// do not show ellipses for strings shorter than 4 characters.
if (truncate < 4) str.substring(0, truncate)
else str.substring(0, truncate - 3) + "..."
} else {
str
}
}: Seq[String]
}
val sb = new StringBuilder
val numCols = df.schema.fieldNames.length
// Initialise the width of each column to a minimum value of '3'
val colWidths = Array.fill(numCols)(3)
// Compute the width of each column
for (row <- rows) {
for ((cell, i) <- row.zipWithIndex) {
colWidths(i) = math.max(colWidths(i), cell.length)
}
}
// Create SeparateLine
val sep: String = colWidths.map("-" * _).addString(sb, "+", "+", "+\n").toString()
// column names
rows.head.zipWithIndex.map { case (cell, i) =>
if (truncate > 0) {
StringUtils.leftPad(cell, colWidths(i))
} else {
StringUtils.rightPad(cell, colWidths(i))
}
}.addString(sb, "|", "|", "|\n")
sb.append(sep)
// data
rows.tail.map {
_.zipWithIndex.map { case (cell, i) =>
if (truncate > 0) {
StringUtils.leftPad(cell.toString, colWidths(i))
} else {
StringUtils.rightPad(cell.toString, colWidths(i))
}
}.addString(sb, "|", "|", "|\n")
}
sb.append(sep)
// For Data that has more than "numRows" records
if (hasMoreData) {
val rowsString = if (numRows == 1) "row" else "rows"
sb.append(s"only showing top $numRows $rowsString\n")
}
sb.toString()
}
I made a console program, but the problem is that it doesn't allow parameters to be inserted. So I'm wondering how would I split a single string into multiple strings to achieve what I need. E.g.: text="msg Hello" would be split into textA="msg" and textB="Hello"
This is the main console code so far (just to show the idea):
if (keyboard_check_pressed(vk_enter)) {
text_console_c = asset_get_index("scr_local_"+string(keyboard_string));
if (text_console_c > -1) {
text_console+= "> "+keyboard_string+"#";
script_execute(text_console_c);
text_console_c = -1;
}
else if (keyboard_string = "") {
text_console+= ">#";
}
else {
text_console+= "> Unknown command: "+keyboard_string+"#";
};
keyboard_string = "";
}
I cant recommend spliting string with iteration by char, because when u try split very very very long string, then time to split is very long and can freeze thread for a short/long time. Game maker is single threaded for now.
This code is much faster.
string_split
var str = argument[0] //string to split
var delimiter = argument[1] // delimiter
var letDelimiter = false // append delimiter to each part
if(argument_count == 3)
letDelimiter = argument[2]
var list = ds_list_create()
var d_at = string_pos(delimiter, str)
while(d_at > 0) {
var part = string_delete(str, d_at , string_length(str))
if(letDelimiter)
part = part + delimiter
str = string_delete(str, 1, d_at)
d_at = string_pos(delimiter, str)
ds_list_add(list, part)
if(d_at == 0 && str != "")//last string without delimiter, need to add too
ds_list_add(list, str)
}
return list;
Dont forget ds_list_destroy after you iterate all strings
for example:
var splited = string_split("first part|second part", '|')
for(splited) {
//do something with each string
}
ds_list_destroy(splited)
Something like this may help, haven't tested it out but if you can follow what is going on its a good place to start.
Text = "msg Hello"
counter = 0
stringIndex = 0
for (i = 0; i < string_length(text); i++)
{
if string_char_at(text,i) == " "
{
counter++
stringIndex = 0
} else {
string_insert(string_char_at(text,i),allStrings(counter),stringIndex)
stringIndex++
}
}
allStrings should be an array containing each of the separate strings. Whenever a " " is seen the next index of allStrings starts having it's characters filled in. stringIndex is used to add the progressive characters.
This question is unlikely to help any future visitors; it is only relevant to a small geographic area, a specific moment in time, or an extraordinarily narrow situation that is not generally applicable to the worldwide audience of the internet. For help making this question more broadly applicable, visit the help center.
Closed 10 years ago.
I need a program that takes a two digit number such as "22" and returns "Twenty Two". I have the following started for the main method but have nothing for the string method
static string TwoDigit(int n)
{
}
static void Main(string[] args)
{
for (int i = 0; i <= 19; i++)
Console.WriteLine("{0}: {1}", i, TwoDigit(i));
for (int i = 20; i <= 110; i += 7)
Console.WriteLine("{0}: {1}", i, TwoDigit(i));
}
I don't know of an existing program but it would be easy to write the function.
I would convert the int to a string then do a switch statement on each character.
The first switch would handle the "Twenty", "Thirty", etc.
The second switch would handle one, two, three, etc.
You will need to have a special case for the teens that just spits out the word for each.
Just take time in your reseach. If you don't know how to do it, I would advise to parse the int number by one number and then format the text via case.
switch (int)
case 1:
cout << "one";
switch (int)
case 2:
cout << "two";
It would be easiest to just evaluate both digits separately and match them up to string values stored in two arrays.
So for example, you might have these two arrays...
tens[0] = ""
tens[1] = ""
tens[2] = "Twenty"
tens[3] = "Thirty"
tens[4] = "Forty"
tens[5] = "Fifty"
tens[6] = "Sixty"
etc...
ones[0] = ""
ones[1] = "One"
ones[2] = "Two"
ones[3] = "Three"
etc...
And then if the number is >= 20, you can simply take the first digit and use it as your index for the tens array, and your second digit and use it as your index for your ones array. If the number is between 10 and 19, you'll need some special logic to handle it.
Initialize this function
function Ones_String(number1) {
if (number1 == 1) {
string1 = "One";
} elseif (number2 == 9) {
string1 = "Nine";
} elseif (number2 == 10) {
string1 = "Ten";
} elseif (number2 == 0) {
string1 = "Zero";
} else {
string1 = ""; // empty value
}
return string1;
}
function Tens_String(number2) {
if (number2 == 2) {
string2 = "Twenty";
} elseif (number2 == 3) {
string2 = "Thirty";
} elseif (number2 == 9) {
string2 = "Ninety";
} else {
string2 = ""; // emtpy value
}
return string2;
}
function teens_string(number3) {
if (number3 == 11) {
string3 = "Eleven";
} elseif (number3 == 12) {
string3 = "Tweleve";
} else {
string3 = "Nineteen";
}
return string3;
}
If given number < 11 then call Ones_string()
If it is number >= 11 then do below logic
First : get the seconds digit value and call Tens_String();
Second : get the first digit vallue and call Ones_string();
And this algorithm applies till 99 .. Last used in 2006 at College on C++..
Whatever i mentioned is an algorithm / logic to detect.. not the perfect code
I have the following code, which works, but I'm wondering if there is a "groovier" way of doing this:
/**
* 10 digit - #-######-##-#
* 13 digit - ###-#-######-##-#
* */
private formatISBN(String isbn) {
if (isbn?.length() == 10) {
def part1 = isbn.substring(0, 1)
def part2 = isbn.substring(1, 7)
def part3 = isbn.substring(7, 9)
def part4 = isbn.substring(9, 10)
return "${part1}-${part2}-${part3}-${part4}"
} else if (isbn?.length() == 13) {
def part1 = isbn.substring(0, 3)
def part2 = isbn.substring(3, 4)
def part3 = isbn.substring(4, 10)
def part4 = isbn.substring(10, 12)
def part5 = isbn.substring(12, 13)
return "${part1}-${part2}-${part3}-${part4}-${part5}"
} else {
return isbn
}
}
You could first use the [] string operator to get the substrings instead of substring and drop the intermediate variables. For example in the case for length == 10:
"${isbn[0]}-${isbn[1..6]}-${isbn[7..8]}-${isbn[9]}"
Now, there is a bit of repetition there. You can get instead first get all the isbn segments and then .join them with '-':
[isbn[0], isbn[1..6], isbn[7..8], isbn[9]].join('-')
And, even further, instead of referencing isbn every time, you can make a list of the ranges you want to get and then get them all the same time using collect:
[0, 1..6, 7..8, 9].collect { isbn[it] }.join('-')
If you're going for code golfing, you can also do:
('-'+isbn)[1, 0, 2..7, 0, 8..9, 0, 10]
I'll leave it to you to figure out how that works, but i guess it's probably not a good idea to leave that on production code, unless you want to surprise future maintainers hehe.
Also, notice that the format when length == 13 is the same as for length == 10 but with a different prefix, you can then reuse the same function in that case. The whole function (with a couple of tests) would be:
/**
* 10 digit - #-######-##-#
* 13 digit - ###-#-######-##-#
**/
def formatIsbn(isbn) {
switch (isbn?.length()) {
case 10: return [0, 1..6, 7..8, 9].collect { isbn[it] }.join('-')
case 13: return isbn.take(3) + '-' + formatIsbn(isbn.drop(3))
default: return isbn
}
}
assert formatIsbn('abcdefghij') == 'a-bcdefg-hi-j'
assert formatIsbn('abcdefghijklm') == 'abc-d-efghij-kl-m'
Now, i think there are some bad smells in that code. Can isbn be null? At least to me, this doesn't look like a function that needs to bother about the nullity of its argument, or at least that's not clear by reading its name (it should be called something like formatIsbnOrNull instead if both ISBN strings and null values are accepted). If null values are not valid, then let it blow up with a NullPointerException when accessing isbn.length() so the caller know they have passed a wrong argument, instead of silently returning the same null.
The same goes for the return ISBN at the end. Is it expected for that function to receive a string that's neither 10 nor 13 characters long? If not, better throw new IllegalArgumentException() and let the caller know they have called it wrongly.
Finally, i'm not sure if this is the most "readable" solution. Another possible solution is having a string for the format, like '###-#-######-##-#' and then replace the #s by the isbn characters. I think it might be more self-documenting:
def formatIsbn(isbn) {
def format = [
10: '#-######-##-#',
13: '###-#-######-##-#'
][isbn.length()]
def n = 0
format.replaceAll(/#/) { isbn[n++] }
}
Consider adding the method to the String class, as shown here. Note that this answer is a spin on a clever suggestion in epidemian's answer (re: collect).
Note:
This code augments String with asIsbn().
The range [0..2] does not need the call to asIsbn(), but the symmetry of using collect twice is irresistable.
Groovy returns the last expression in if/else, so 'return' is not necessary
/**
* 10 digit - #-######-##-#
* 13 digit - ###-#-######-##-#
**/
String.metaClass.asIsbn = { ->
if (delegate.length() == 10) {
[0, 1..6, 7..8, 9].collect { delegate[it] }.join('-')
} else if (delegate.length() == 13) {
[0..2, 3..12].collect { delegate[it].asIsbn() }.join('-')
} else {
delegate
}
}
assert "abcdefghij".asIsbn() == 'a-bcdefg-hi-j'
assert "abcdefghijklm".asIsbn() == 'abc-d-efghij-kl-m'
assert "def".asIsbn() == "def"
String s = null
assert s?.asIsbn() == null
I would try using Regex... I think it's pretty much readable if you know how to use regex, and it's javascript inspired syntax in groovy is pretty cool also.
One more thing: it's pretty clear, looking at the capture groups, what your string looks like for the desired formatting.
private formatISBN(String isbn) {
if (isbn?.length() == 10) {
m = isbn =~ /(\d{1})(\d{6})(\d{2})(\d{1})/
return "${m[0][1]}-${m[0][2]}-${m[0][3]}-${m[0][4]}"
} else if (isbn?.length() == 13) {
m = isbn =~ /(\d{3})(\d{1})(\d{6})(\d{2})(\d{1})/
return "${m[0][1]}-${m[0][2]}-${m[0][3]}-${m[0][4]}-${m[0][5]}"
} else {
return isbn
}
}
Btw, #epidemian suggestion using backreferences is great! I think the code would look like:
private formatISBN(String isbn) {
if (isbn?.length() == 10) {
return isbn.replaceAll(/(\d{1})(\d{6})(\d{2})(\d{1})/, '$1-$2-$3-$4')
} else if (isbn?.length() == 13) {
return isbn.replaceAll(/(\d{3})(\d{1})(\d{6})(\d{2})(\d{1})/, '$1-$2-$3-$4-$5')
} else {
return isbn
}
}
Dunno if I like this any better. I'd make the position map a static final, too.
private isbnify(String isbn) {
def dashesAt = [ 10: [[0,1], [1,7], [7,9], [9,10]],
13: [[0,3], [3,4], [4,10], [10,12], [12,13]]]
def dashes = dashesAt[isbn?.length()]
(dashes == null) ? isbn
: dashes.collect { isbn.substring(*it) }.join('-')
}
Ranges make for a bit less clutter, IMO:
private isbnify3(String isbn) {
def dashesAt = [ 10: [0, 1..6, 7..8, 9],
13: [0..2, 3, 4..9, 10..11, 12]]
def dashes = dashesAt[isbn?.length()]
dashes == null ? isbn : dashes.collect { isbn[it] }.join("-")
}
With an inject-with-two-accumulators it should be easy to do a list-of-dash-positions version, too.
This should be a comment to #everton, but I don't have the 50 reputation needed to do that yet. So this answer is really just a suggested variation on #everton's answer.
One less regex by making the first 3 digits optional. The downside is having to remove a leading '-' if the ISBN is 10 characters. (I also prefer \d over \d{1}.)
private formatISBN(String isbn) {
String result = isbn.replaceAll(/^(\d{3})?(\d)(\d{6})(\d{2})(\d)$/,
'$1-$2-$3-$4-$5')
if (result) {
return result.startsWith('-') ? result[1..-1] : result
} else {
return isbn // return value unchanged, pattern didn't match
}
}
println formatISBN('1234567890')
println formatISBN('9991234567890')
println formatISBN('123456789') // test an ISBN that's too short
println formatISBN('12345678901234') // test an ISBN that's too long