[Question]
  [
**Monday Mini-Golf:** A series of short [code-golf](/questions/tagged/code-golf "show questions tagged 'code-golf'") challenges, posted (hopefully!) every Monday.
Many web applications (especially social media) automatically truncate long passages of text so they fit within the app's formatting. In this challenge, we're going to create an algorithm to automatically trim a passage of text to a certain length.
# Challenge
The goal of the challenge is to write a program or function that takes in two arguments:
* *T*, the text to truncate.
* *L*, the maximum length to return.
And returns *T*, truncated with the following logic:
* If the length of *T* is less than or equal to *L*, no truncation is needed. Return the original string.
* Truncate *T* to length *L*-2. If this contains no spaces or hyphens, return *T* truncated to exactly *L*-3 characters, followed by an ellipsis `...`.
* Otherwise, trim the end of the result up to the last space or hyphen. Add an ellipsis `...` and return the result.
# Details
* *T* and *L* may be taken in either order and any format.
* You may assume that 3 < *L* < 231.
* You may not use U+2026 Horizontal Ellipsis `…`; you must use three periods.
* The input will not start with a space or a hyphen.
* The input will not contain any whitespace other than regular spaces. (No tabs, newlines, etc.)
# Test-cases
### Inputs:
```
"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
```
### Outputs:
```
"This is some very long..."
"This-is-some-long-hyphen..."
"Programming Puzzles & Code Golf is a question and..."
"abcdefghijklmnopq..."
"a..."
"Very long."
```
(Note that the quotes are just to specify that these are strings; they need not be included.)
# Scoring
This is [code-golf](/questions/tagged/code-golf "show questions tagged 'code-golf'"), so shortest valid code in bytes wins. Tiebreaker goes to submission that reached its final byte count first. ~~The winner will be chosen next Monday, Oct 5. Good luck!~~
**Edit:** Congrats to your winner, @Jakube with Pyth again, with **25 bytes!**
  
[Answer]
# Pyth, 25 bytes
```
+WnzK<zeo}@zN" -"-Q2K"...
```
Try it online: [Demonstration](http://pyth.herokuapp.com/?code=%2BWnzK%3Czeo%7D%40zN%22+-%22-Q2K%22...&input=This+is+some-very+long+text.%0A25&test_suite_input=This+is+some+very+long+text.%0A25%0AThis-is-some-long-hyphen-separated-text.%0A33%0AProgramming+Puzzles+%26+Code+Golf+is+a+question+and+answer+site+for+programming+puzzle+enthusiasts+and+code+golfers.%0A55%0Aabcdefghijklmnopqrstuvwxyz%0A20%0Aa+b+c%0A4%0AVery+long.%0A100&debug=0&input_size=2) or [Test Suite](http://pyth.herokuapp.com/?code=%2BWnzK%3Czeo%7D%40zN%22+-%22-Q2K%22...&input=This+is+some-very+long+text.%0A25&test_suite=1&test_suite_input=This+is+some+very+long+text.%0A25%0AThis-is-some-long-hyphen-separated-text.%0A33%0AProgramming+Puzzles+%26+Code+Golf+is+a+question+and+answer+site+for+programming+puzzle+enthusiasts+and+code+golfers.%0A55%0Aabcdefghijklmnopqrstuvwxyz%0A20%0Aa+b+c%0A4%0AVery+long.%0A100&debug=0&input_size=2)
### Explanation:
```
+WnzK<zeo}@zN" -"-Q2K"...  implicit: z = input string, Q = input number
        o        -Q2       order the indices N in [0, 1, ..., Q-3] by
         }@zN" -"            z[T] in " -"
                           (hyphen-indices get sorted to the back)
       e                   take the last such number
     <z                    reduce z to length ^
    K                      save this string to K
+WnzK               K"...  print (K + "...") if z != K else only K
```
[Answer]
# Perl, ~~69~~ ~~59~~ 52 bytes
51 bytes code + 1 byte command line. Assumes numerical input is allowed to be given with -i parameter.
```
s/.{$^I}\K.*//&&s/(^([^ -]*).|.*\K[ -].*)..$/$2.../
```
Usage:
```
echo "This-is-some-long-hyphen-separated-text." | perl -p -i"33" entry.pl
```
[Answer]
# Python 2, ~~78~~ 73 bytes
```
t,l=input()
u=t[:l-2]
print(t,u[:max(map(u.rfind,' -'))]+'...')[l<len(t)]
```
Input format follows the example input. 
[Answer]
# JavaScript (ES6), ~~123~~ ~~78~~ ~~67~~ 61 bytes
I didn't expect to be able to cut this down so much, but it turns out the splice/replace combo is able to cover every case where truncation is needed.
```
(T,L)=>T[L]?T.slice(0,L-2).replace(/([ -][^ -]*|.)$/,'...'):T
```
First argument is the string, second is the length. Special thanks to edc65 for the length check optimization!
Here's the original code (123 bytes):
```
(T,L)=>(T.length>L?(S=T.slice(0,L)).slice(0,(m=Math.max(S.lastIndexOf` `,S.lastIndexOf`-`))<0?L-3:Math.min(L-3,m))+'...':T)
```
[Answer]
# TI-BASIC, 87 bytes
```
Prompt L,Str1
For(X,1,L
{inString(Str1," ",X),inString(Str1,"-",X
max(I,max(Ans*(Ans≤L-3->I
End
Str1
If L<length(Ans
sub(Ans,1,I+(L-3)not(I))+"...
Ans
```
TI-BASIC doesn't have many string manipulation commands, so we need to find the last index manually: if the string doesn't contain the string to search for, `inString(` returns 0. We search for hyphens and spaces starting at every position from 1 to `L`, and record the greatest number less than or equal to `L-3`. If that number `I` is still 0, we use `L-3` as the ending index instead.
Due to the calculator's limitations, the largest addressable index of a string is 9999; therefore, this will fail for larger strings.
I rely on the calculator's behavior of automatically initializing the variable `I` to 0, so delete `I` or clear your calculator's memory before running.
[Answer]
# Python 2, 105 bytes
```
def t(s,l):a=s[:l-2];return s[:max(a.rfind(' '),a.rfind('-'))]+'...'if' 'in a or'-'in a else a[:-1]+'...'
```
Called with
```
>>> print t("This is some very long text.", 25)
This is some very long...
```
[Answer]
# C# .NET, ~~187~~ 169 bytes
Hmm...
```
string f(string T,int L){if(T.Length<=L)return T;T=T.Substring(0,L-2);return T.Substring(0,T.Contains(" ")||T.Contains("-")?T.LastIndexOfAny(new[]{' ','-'}):L-3)+"...";}
```
[Answer]
# Groovy, 95 bytes
```
a={T,L->F=T.size()<=L?T:T[0..L-3]
m=F=~'(.*[- ])'
F==T?F:m?m[0][0].trim()+'...':F[0..-2]+'...'}
```
Pretty straightforward, can probably be golfed further
[Answer]
# CJam, 34 bytes
```
q~1$<_@={-2<_W%{" -"&}#~We|<'.3*}|
```
Try it online: [Chrome](http://cjam.aditsu.net/#code=q~1%24%3C_%40%3D%7B-2%3C_W%25%7B%22%20-%22%26%7D%23~We%7C%3C'.3*%7D%7C&input=%22a%20b%20c%22%204) | [Firefox](http://cjam.aditsu.net/#code=q~1%24%3C_%40%3D%7B-2%3C_W%2525%7B%22%20-%22%26%7D%23~We%7C%3C'.3*%7D%7C&input=%22a%20b%20c%22%204)
[Answer]
**T-SQL,145 bytes**
```
create proc a(@t varchar(max),@l int)as if LEN(@t)<=@l return @t;set @t = LEFT(@t,@l-3) select LEFT(@t,LEN(@t)-CHARINDEX('-',REVERSE(@t)))+'...'
```
usage:
`exec a("This is some very long text.", 25)
 exec a("This-is-some-long-hyphen-separated-text.", 33)`
[Answer]
# [rs](https://github.com/kirbyfan64/rs), 116
```
(\d+)$/(_)^^(\1)
___$/
\t
+\t(.)(.*) (_)(_*)$/\1\t\2 \4
\t(.*?)_+$/\1\t
\t( ?).+/\1
[- ][^- \t]*$/
(?<!\t)$/...
 \t/
```
At least it's shorter than C#...
[Live demo and test cases.](http://kirbyfan64.github.io/rs/index.html?script=%28%5Cd%2B%29%24%2F%28_%29%5E%5E%28%5C1%29%0A___%24%2F%0A%5Ct%0A%2B%5Ct%28.%29%28.*%29%20%28_%29%28_*%29%24%2F%5C1%5Ct%5C2%20%5C4%0A%5Ct%28.*%3F%29_%2B%24%2F%5C1%5Ct%0A%5Ct%28%20%3F%29.%2B%2F%5C1%0A%5B-%20%5D%5B%5E-%20%5Ct%5D*%24%2F%0A%28%3F%3C!%5Ct%29%24%2F...%0A%C2%A0%5Ct%2F&input=This%20is%20some%20very%20long%20text.%2025%0AThis-is-some-long-hyphen-separated-text.%2033%0AProgramming%20Puzzles%20%26%20Code%20Golf%20is%20a%20question%20and%20answer%20site%20for%20programming%20puzzle%20enthusiasts%20and%20code%20golfers.%2055%0Aabcdefghijklmnopqrstuvwxyz%2020%0Aa%20b%20c%204%0AVery%20long.%20100)
[Answer]
# 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)`](https://modules.ceylon-lang.org/repo/1/ceylon/language/1.1.0/module-doc/api/Ranged.type.html#measure), 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](https://modules.ceylon-lang.org/repo/1/ceylon/language/1.1.0/module-doc/api/Ranged.type.html#span) from index y to z, both inclusive, as well as half-open spans `x[...z]` and `x[y...]`.)
[`List.lastIndexWhere`](https://modules.ceylon-lang.org/repo/1/ceylon/language/1.1.0/module-doc/api/List.type.html#lastIndexWhere) takes a predicate (i.e. a function taking a list element and returning a boolean, i.e. here a [`Callable<Boolean, [Character]>`](https://modules.ceylon-lang.org/repo/1/ceylon/language/1.1.0/module-doc/api/Callable.type.html)), 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](http://ceylon-lang.org/documentation/1.1/reference/operator/in/) maps to the method Category.contains, which brings us to the idea that we can pass that tuple's [`contains`](https://modules.ceylon-lang.org/repo/1/ceylon/language/1.1.0/module-doc/api/Tuple.type.html#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`](http://ceylon-lang.org/documentation/1.1/reference/operator/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));
    }
}
```
[Answer]
POSIX shell + GNU sed, 65 bytes
```
sed -re "s/(.{$1}).+/\1/;T;s/(.*)[- ]...*/\1.../;t;s/...$/.../;:"
```
This is a job made for sed! But I needed shell to get the length limit in (perhaps Perl would be better). The sed part expands out to a fairly simple sequence, with conditional jumps when we finish:
```
s/(.{$1}).+/\1/
T
s/(.*)[- ]...*/\1.../
t
s/...$/.../
:
```
[Answer]
# Mathematica 192 bytes
```
t=With[{r=StringTake[#,Min[#2-2,StringLength[#]]],p={"-",Whitespace},e="..."}, 
  Which[StringLength[#]<=#2,#,StringFreeQ[r,p],StringDrop[r,-1]<>e,
   True,StringTake[r,Max[StringPosition[r,p]]-1]<>e]]&
```
Called as
```
t["This is some very long text.", 25]
```
[Answer]
# ><>, 74 bytes
```
l$-:1)?\~r05.
/?=0:~$<-1
\}:0=  ?\::"- "@=@=+?
>~"..."r\}
/!?     <
>ol?!;
```
This solution requires the string to be truncated and `L` to already be on the stack, in that order.
There's 7 wasted bytes caused by alignment issues, still trying to golf those out.
[Answer]
## C# (157):
Based on the [**answer of Salah Alami**](https://codegolf.stackexchange.com/a/58957/15235), but shorter. The ***string*** class derives from `IEnumerable<char>`, so instead of `T.Contains(" ")||T.Contains("-")`, I use `" -".Any(x=>T.Contains(x))`.
Solution:
```
string f(string T,int L){if(T.Length<=L)return T;T=T.Substring(0,L-2);return T.Substring(0," -".Any(T.Contains)?T.LastIndexOfAny(new[]{' ','-'}):L-3)+"...";}
```
Ungolfed:
```
string f (string T, int L)
{
    if (T.Length <= L)
        return T;
    T = T.Substring(0, L - 2);
    return T.Substring(0, " -".Any(T.Contains) ? T.LastIndexOfAny(new[]{' ', '-'}) : L - 3) + "...";
}
```
**Update:**
Saved 6 bytes thanks to the comment of SLuck49, using `Any(T.Contains)` instead of `Any(x=>T.Contains(x))`.
[Answer]
## [GS2](https://github.com/nooodl/gs2), 29 bytes
This program takes standard input. The first line is the string, and the second is the target length number.
```
2a 0e 56 3c 40 a0 74 20 22 22 04 5d 2e 2a 3f 5b
20 2d 5d 7c 2e 07 2e 2e 2e 9d 20 e4 35
```
GS2 code can be a bit hard to read sometimes. :) Here's some commentary.
```
2a         # lines - split input on newlines yielding a two element array
0e         # extract-array - pop array, push both elements 
56         # read-num - convert length string to number
3c         # take - truncate the string to specified length
40         # dup - duplicate truncated string on stack
a0         # junk1 - push the last popped value, the un-truncated string
74         # ne - test for inequality
    20     # reverse string
    22 22  # tail tail - remove first two characters
    # regex replace first occurrence of ".*?[ -]|." with "..."
    04 5d 2e 2a 3f 5b 20 2d 5d 7c 2e 07 2e 2e 2e 9d 
    20     # reverse string
e4         # block5 - make a block out of last 5 instructions
35         # when - conditionally execute block
```
[Answer]
# Groovy, 56 bytes
Copied Kleyguerth's answer first, hence the same variable names...
Trim the string down by 2 chars, then most of the work is done by the regex, replace a dash or a space followed by any number of chars that are not a dash or a space at the end of the string with a "." OR replace any character at the end of the string if all the chars before it are neither a dash or a space with a ".". Harder to put into words than to write the regex...
```
a={T,L->T.size()<=L?T:T[0..L-3].replaceAll("([- ][^ -]*|(?<=[^- ]*).)\$",".")+".."}
```
Edit: Actually, can just remove the part of the string that matches the regex and add "..." at the end:
```
a={T,L->T.size()<=L?T:T[0..L-3]-~/[- ][^ -]*$|.$/+"..."}
```
[Answer]
# [Jelly](https://github.com/DennisMitchell/jelly), 29 bytes
```
ḣṖṖe€⁾ -œṗƊṖȯ$Ẏḣ⁹_3¤”.⁺⁺ðL>¥¡
```
[Try it online!](https://tio.run/##y0rNyan8///hjsUPd04DotRHTWseNe5T0D06@eHO6ce6gEIn1qs83NUHVPGocWe88aEljxrm6j1q3AVEhzf42B1aemjh////lQKK8tOLEnNzM/PSFQJKq6pyUosV1BSc81NSFdzzc9IUMosVEhUKS1OLSzLz8xQS81KAuLg8tUihOLMkVSEtv0ihAMmEArAJCql5JRmlxZmJxSXFYC3JIOPSgcalFhXrKf03NQUA "Jelly – Try It Online")
Full program.
[Answer]
# [Clean](https://github.com/Ourous/curated-clean-linux), 89 bytes
```
import StdEnv
$n l|size l>n=l%(0,last[n:[i\\i<-[2..n]&c<-:l|c==' '||c=='-']]-3)+++"..."=l
```
[Try it online!](https://tio.run/##JYyxasMwFAD3foUwSZ3iPFHqzVidkqHQIZBsjoeHLNuCpydjqSEp/vYooYWD45bTZJCT890PGeHQcrJu8nMUx9jt@fKyYkFLsL9G0CcrWm/et4QhNlw19ny2NTQfUnL7qmuoaNFK5SJf/gx520L5VhRFJqXMFKVjxOdYiZUoS5GdRhvgSfDOAHkeYLxNo2EIZsIZo@kgmmuUWbrrnnAICb6@0@7G6Kz@jwNh7P3sHg "Clean – Try It Online")
As a function `$ :: Int String -> String`
[Answer]
# [C# (Visual C# Interactive Compiler)](http://www.mono-project.com/docs/about-mono/releases/5.0.0/#csc), 117 bytes
```
a=>b=>a.Length>b?a.Substring(0,(" -".Any(x=>a.IndexOf(x,0,b-2)>-1)?a.LastIndexOfAny(new[]{' ','-'},b-2):b-3))+"...":a
```
Based off of @Abba's, which is based off of @Salah Alami's answer. Instead of using `Contains` and an unnecessary `Substring` call, it uses IndexOf to check if a hyphen or space exists in the truncated string.
[Try it online!](https://tio.run/##TVFRS8MwEH7vrzj64FqWlLq5F7tWRFCEgYKiD2MPaXtrY7ekS9Ktm/jbZ7KpCJdw4fvuu@9yhaaF5sf7ThRTbRQXFTnlXBhyfmdZnx5ZmuVpxqIZisrUWX7DopcuPxOCmAQ@UD@6Ffugd6xHUWL/tAx6EpOcjsKMXoa2Ysa0@YEcVeBuvvgcwIAM6ODrRLzO6TgMh34URf41OybeucN80abep@e/1lyDDS3XCFtUe1hJUYHB3kQ@OePUhsOpg2i9b2sUVGPLFDNY0j/us5KVYuu1lYfn7nBYoYYLuJMlwoNcLV0bBpsOteFSABOlPXqHCjQ3CEupoP2n0J4UAIWpO83tnPpUUji5ysqh0rYreD7LixKXVc0/mtVayHajtOm2u35/cKYY5FC45O13uMj/Sjy7jPlCui8YTch4TCYTMorJFbmMY8/C1kywZQqaNE6aafuzpKQZDkPvXVm7My4w6IN23izCQLo7TI7f "C# (Visual C# Interactive Compiler) – Try It Online")
]