Ceylon 386 333 252 230 222 216 171 153 131 111
String t(String s,Integer l)=>s.size<l then s else s[0:(s[0:l-2].lastIndexWhere(" -".contains)else l-3)]+"...";
Ungolfed Original:
String truncate(String text, Integer length) {
if(text.size < length) {
return text;
}
Boolean spacePredicate(Character char) {
return char == ' ' || char == '-';
}
Integer? spaceIndex = text[0:length-2].lastIndexWhere(spacePredicate);
if(exists spaceIndex) {
return text[0:spaceIndex] + "...";
}
return text[0:length-3]+"...";
}
This is 386 bytes/characters.
Some interesting features here:
The x[y:z]
syntax is syntactic sugar for x.measure(y, z)
, and returns a subrange of x
starting at y
with length z
– for strings, this is a substring. (There is also x[y..z]
syntax, which is a span from index y to z, both inclusive, as well as half-open spans x[...z]
and x[y...]
.)
List.lastIndexWhere
takes a predicate (i.e. a function taking a list element and returning a boolean, i.e. here a Callable<Boolean, [Character]>
), and gives the index of the last list element where the predicate is fulfilled (or null, if it's never fulfilled). As strings are lists, this works for strings too.
The result of this, spaceIndex
is of type Integer|Null
, or Integer?
for short – i.e. it can either be an Integer or null
(the only value of type Null
). (The name spaceIndex
comes from when I didn't realize that -
was also special – I guess breakIndex
would be better.)
With exists spaceIndex
we can check if spaceIndex
is non-null, and do something different then. (Inside this if-block the compiler knows that it is non-null ... without that it would have complained if I used spaceIndex
to access the string.)
Instead of the local function spacePredicate
we can also use an anonymous function
(Character char) => char == ' ' || char == '-'
This brings us to 333 characters:
String truncate(String text, Integer length) {
if(text.size < length) {
return text;
}
Integer? spaceIndex = text[0:length-2].lastIndexWhere(
(Character char) => char == ' ' || char == '-');
if(exists spaceIndex) {
return text[0:spaceIndex] + "...";
}
return text[0:length-3]+"...";
}
Next optimization is to use shorter variable and function names, which brings us down by 81 bytes to 252:
String t(String s, Integer l) {
if(s.size < l) {
return s;
}
Integer? i = s[0:l-2].lastIndexWhere(
(Character e) => e == ' ' || e == '-');
if(exists i) {
return s[0:i] + "...";
}
return s[0:l-3]+"...";
}
The predicate function actually doesn't need its argument type declared, that can be inferred by the compiler. Same for the type of i
(where we still have to write value
to mark it as a declaration). Now that declaration is short enough to fit on one line, bringing us down to 230:
String t(String s, Integer l) {
if(s.size < l) {
return s;
}
value i = s[0:l-2].lastIndexWhere((e) => e == ' ' || e == '-');
if(exists i) {
return s[0:i] + "...";
}
return s[0:l-3]+"...";
}
Instead of e == ' ' || e == '-'
we can also write e in [' ', '-']
(or e in {' ', '-'}
, this is an iterable constructor instead of a tuple one).
The in
operator maps to the method Category.contains, which brings us to the idea that we can pass that tuple's contains
method directly (it is a callable taking any object, so also accepting character), without the (e) => ...
boilerplate (222 bytes):
String t(String s, Integer l) {
if(s.size < l) {
return s;
}
value i = s[0:l-2].lastIndexWhere([' ', '-'].contains);
if(exists i) {
return s[0:i] + "...";
}
return s[0:l-3]+"...";
}
Actually, another category containing the same two characters is the two-character string " -"
. (In addition it also contains its substrings, but that doesn't hurt here). 216 bytes.
String t(String s, Integer l) {
if(s.size < l) {
return s;
}
value i = s[0:l-2].lastIndexWhere(" -".contains);
if(exists i) {
return s[0:i] + "...";
}
return s[0:l-3]+"...";
}
I guess we got the most out of this line, let's turn to the others ... the last two return statements have some similarity which we can exploit – they just differ in i
vs. l-3
, and are using i
just when it is not null, otherwise l-3
. Fortunately this is exactly what the else
operator is made for!
String t(String s, Integer l) {
if(s.size < l) {
return s;
}
value i = s[0:l-2].lastIndexWhere(" -".contains);
return s[0:(i else l-3)] + "...";
}
(The parentheses seem to be needed here, as else
has a lower precedence than [:]
.) This is 171 characters. Now i
is used just once, so we can inline it, bringing us to 153 characters:
String t(String s, Integer l) {
if(s.size < l) {
return s;
}
return s[0:(s[0:l-2].lastIndexWhere(" -".contains) else l-3)] + "...";
}
We can also replace this if-return-return
combination by a combination of the then
and else
operators in one return
. (then
returns is second operand when the first one is true, otherwise null, which then allows else
to return its second operand.`) 131 bytes (although some of the savings are the white spaces which we'll get rid off anyways):
String t(String s, Integer l) {
return s.size < l then s else s[0:(s[0:l-2].lastIndexWhere(" -".contains) else l-3)] + "...";
}
A function which contains just one return with an expression can alternatively be written with the "fat arrow" notation, giving 123:
String t(String s, Integer l) =>
s.size < l then s else s[0:(s[0:l-2].lastIndexWhere(" -".contains) else l-3)] + "...";
Removing the unneeded whitespace gives us the final 111 bytes:
String t(String s,Integer l)=>s.size<l then s else s[0:(s[0:l-2].lastIndexWhere(" -".contains)else l-3)]+"...";
As an addition, here is a function which prints the examples from the question (using the name t
which is used after step two):
shared void testTruncate() {
value testInputs = {
["This is some very long text.", 25],
["This-is-some-long-hyphen-separated-text.", 33],
["Programming Puzzles & Code Golf is a question and answer site for programming puzzle enthusiasts and code golfers.", 55],
["abcdefghijklmnopqrstuvwxyz", 20],
["a b c", 4],
["Very long.", 100]
};
for(input in testInputs) {
print(t(*input));
}
}