25
\$\begingroup\$

Given a string, find the first word starting with each letter (case insensitive).

Sample

Using Ferulas flourish in gorgeous gardens. as input:

"Ferulas flourish in gorgeous gardens."
 ^^^^^^^          ^^ ^^^^^^^^
 |                |  |
 |                |  --> is the first word starting with `g`
 |                --> is the first word starting with `i`
 --> is the first word starting with `f`

Then, the output for this sample should be the matched words joined by one single space:

"Ferulas in gorgeous"

Challenge

Both input and output must be a string representation, or the closest alternative in your language.

Program or function allowed.

You can consider a word being at least one of: lowercase or uppercase letters, digits, underscore.

This is , shortest answer in bytes wins.

Another samples:

input: "Take all first words for each letter... this is a test"
output: "Take all first words each letter is"

input: "Look ^_^ .... There are 3 little dogs :)"
output: "Look _ There are 3 dogs"

input: "...maybe some day 1 plus 2 plus 20 could result in 3"
output: "maybe some day 1 plus 2 could result in 3"
\$\endgroup\$
  • \$\begingroup\$ Are trailing/starting spaces allowed? <s>Can I assume words are separated by one space in original string?</s> \$\endgroup\$ – Qwertiy Apr 8 '16 at 10:10
  • \$\begingroup\$ Iunderstood it from the examples, so there is <s></s> in the comment. What about trimming spaces? \$\endgroup\$ – Qwertiy Apr 8 '16 at 10:47

22 Answers 22

17
\$\begingroup\$

Retina, 28 bytes:

M!i`\b(\w)(?<!\b\1.+)\w*
¶
 
  • M! - Match each work and print all words separated by newlines.
  • i - Ignore case.
  • \b(\w) - Capture first letter of each word
  • (?<!\b\1.+) - After matching the letter, check if there wasn't a previous word starting with the same letter. \1.+ ensures at least two characters, so we are skipping the current word.
  • \w* - match the rest of the word.
    The above matches only words - all other characters are removed.
  • ¶\n - Replace newlines with spaces.

Try it online!

\$\endgroup\$
9
\$\begingroup\$

Retina, 45 bytes

i`\b((\w)\w*)\b(?<=\b\2\w*\b.+)

\W+
 
^ | $

Simply uses a single regex to remove later words starting with the same \w character (case insensitive with the i option), converts runs of \W to a single space, then removes any leading/trailing space from the result.

Try it online!

Edit: See @Kobi's answer for a shorter version using M!`

\$\endgroup\$
  • \$\begingroup\$ Darn it, barely beat me to it! I couldn't figure out the lookbehind though. \$\endgroup\$ – GamrCorps Apr 7 '16 at 3:58
  • 3
    \$\begingroup\$ I've added another Retina answer - I think that's OK if they are different enough (the basic concept is similar, of course). \$\endgroup\$ – Kobi Apr 7 '16 at 6:23
  • 1
    \$\begingroup\$ @Kobi It's much better, so I'm glad to see it :) Makes me realise how much more I need to learn about Retina's line options and what not. \$\endgroup\$ – Sp3000 Apr 7 '16 at 6:26
  • \$\begingroup\$ Could you do this to save a few bytes? i` \b((\w)\w*)\b(?<=\b\2\w*\b.+) (a space before the first \b) Are the lines afterwards unnecessary? \$\endgroup\$ – Leaky Nun Apr 8 '16 at 1:19
  • \$\begingroup\$ @KennyLau Unfortunately, I don't think that works because words aren't necessarily separated by spaces, e.g. a...a -> a \$\endgroup\$ – Sp3000 Apr 8 '16 at 2:36
9
\$\begingroup\$

JavaScript (ES6), 73 71 bytes

s=>s.match(u=/\w+/g).filter(w=>u[n=parseInt(w[0],36)]?0:u[n]=1).join` `

Saved 2 bytes thanks to @edc65!

Test

var solution = s=>s.match(u=/\w+/g).filter(w=>u[n=parseInt(w[0],36)]?0:u[n]=1).join` `;
var testCases = [
  "Ferulas flourish in gorgeous gardens.",
  "Take all first words for each letter... this is a test",
  "Look ^_^ .... There are 3 little dogs :)",
  "...maybe some day 1 plus 2 plus 20 could result in 3"
];
document.write("<pre>"+testCases.map(t=>t+"\n"+solution(t)).join("\n\n")+"</pre>");

\$\endgroup\$
  • \$\begingroup\$ Using parseInt("_",36) = NaN? Blasphemy! \$\endgroup\$ – Sp3000 Apr 7 '16 at 5:00
  • 1
    \$\begingroup\$ The fun fact is: it works @Sp3000 \$\endgroup\$ – edc65 Apr 7 '16 at 6:57
  • \$\begingroup\$ Using u=regexp is really clever. Save 2 bytes s=>s.match(u=/\w+/g).filter(w=>u[w=parseInt(w[0],36)]?0:u[w]=1).join' ' \$\endgroup\$ – edc65 Apr 7 '16 at 7:00
  • \$\begingroup\$ @edc65 Thanks. It's actually quite convenient that there are 37 possible outputs for a single base-36 digit. \$\endgroup\$ – user81655 Apr 7 '16 at 7:04
7
\$\begingroup\$

Pyth, 23 bytes

J:z"\w+"1jdxDJhM.grhk0J

Try it online: Demonstration or Test Suite

J:z"\w+"1 finds all the words in the input using the regex \w+ and stores them in J.

.grhk0J groups the words by their lowercase first letter, hM takes the first from each group, xDJ sorts these words by their index in the input string, and jd puts spaces between them.

\$\endgroup\$
4
\$\begingroup\$

Perl 6, 39 bytes

{.words.grep({!%.{.substr(0,1).lc}++})}
\$\endgroup\$
  • 1
    \$\begingroup\$ 42 bytes which fixes the words having to match \w+ and golfs the substr part \$\endgroup\$ – Jo King Dec 29 '18 at 12:26
3
\$\begingroup\$

C, 142 132 122 bytes

10 bytes lighter thanks to @tucuxi!

b[200],k;main(c){for(;~c;isalnum(c)|c==95?k&2?:(k|=!b[c|32]++?k&1?putchar(32):0,7:2),k&4?putchar(c):0:(k&=1))c=getchar();}

Prints a trailing space after the last output word.

\$\endgroup\$
  • 1
    \$\begingroup\$ you can shave the checks for c>47 and c<58 by using isalnum instead of isalpha \$\endgroup\$ – tucuxi Apr 7 '16 at 13:43
3
\$\begingroup\$

MATL, 23 bytes

'\w+'XXtck1Z)t!=XRa~)Zc

This borrows Jakube's idea of using a regexp for removing unwanted characters and splitting at the same time.

Input is a string enclosed by single quotes.

Try it online!

Explanation

'\w+'XX  % find words that match this regexp. Gives a cell array
t        % duplicate
c        % convert into 2D char array, right-padded with spaces
k        % make lowercase
1Z)      % get first column (starting letter of each word)
t!=      % duplicate, transpose, test for equality: all combinations  
XR       % set diagonal and below to 0
a~       % true for columns that contain all zeros       
)        % use as a logical index (filter) of words to keep from the original cell array
Zc       % join those words by spaces
\$\endgroup\$
2
\$\begingroup\$

Vim 57 keystrokes

:s/[^a-zA-Z_ ]//g<cr>A <cr>ylwv$:s/\%V\c<c-v><c-r>"\h* //eg<c-v><cr>@q<esc>0"qDk@q

Explanation:

:s/[^a-zA-Z_ ]//g                                 #Remove all invalid chars.
A <cr>                                            #Enter insert mode, and enter 
                                                  #a space and a newline at the end
ylwv$:s/\\c%V<c-v><c-r>"\h* //eg<c-v><cr>@q<esc>  #Enter all of this text on the 
                                                  #next line

0                                                 #Go to the beginning of the line
"qD                                               #Delete this line into register
                                                  #"q"
k@q                                               #Run "q" as a macro  

#Macro
ylw                                               #Yank a single letter
   v$                                             #Visual selection to end of line
     :s/                                          #Substitute regex
       \%V\c                                      #Only apply to the selection and 
                                                  #ignore case
            <c-v><c-r>"                           #Enter the yanked letter
                       \h*                        #All "Head of word" chars
                                                  #And a space
                           //                     #Replace with an empty string
                             eg                   #Continue the macro if not found
                                                  #Apply to all matches
                               <c-v><cr>          #Enter a <CR> literal
                                        @q<esc>   #Recursively call the macro

I'm really dissapointed by how long this one is. The "Invalid" chars (everything but a-z, A-Z, _ and space) really threw me off. I'm sure there's a better way to do this:

:s/[^a-zA-Z_ ]//g

Since \h matches all of that expect for the space, but I can't figure out how to put the metachar in a range. If anyone has tips, I'd love to hear em.

\$\endgroup\$
  • 3
    \$\begingroup\$ why a-zA-Z_ and not \w? digits are valid \$\endgroup\$ – edc65 Apr 7 '16 at 7:18
2
\$\begingroup\$

Julia, 165 155 151 129 102 bytes

g(s,d=[])=join(filter(i->i!=0,[(c=lcfirst(w)[1])∈d?0:(d=[d;c];w)for w=split(s,r"\W",keep=1<0)])," ")

This is a function that accepts a string and returns a string.

Ungolfed:

function g(s, d=[])
    # Split the string into an array on unwanted characters, then for
    # each word, if the first letter has been encountered, populate
    # this element of the array with 0, otherwise note the first letter
    # and use the word. This results in an array of words and zeros.
    x = [(c = lcfirst(w)[1]) ∈ d ? 0 : (d = [d; c]; w) for w = split(s, r"\W", keep=1<0)]

    # Remove the zeros, keeping only the words. Note that this works
    # even if the word is the string "0" since 0 != "0".
    z = filter(i -> i != 0, x)

    # Join into a string and return
    return join(z, " ")
end

Saved 53 bytes with help from Sp3000!

\$\endgroup\$
2
\$\begingroup\$

Jelly, 32 31 bytes

ØB;”_
e€¢¬œṗf€¢¹ÐfµZḢŒlQi@€$ịj⁶

Try it online!

\$\endgroup\$
2
\$\begingroup\$

C# (LINQPAD) - 136 128 bytes

var w=Util.ReadLine().Split(' ');string.Join(" ",w.Select(s=>w.First(f=>Regex.IsMatch(""+f[0],"(?i)"+s[0]))).Distinct()).Dump();
\$\endgroup\$
2
\$\begingroup\$

05AB1E, 40 bytes

Code:

94L32+çJžj-DU-ð¡""Kvy¬Xsl©åï>iX®«Uy}\}ðý

Try it online!

Explanation:

We first generate all characters which should be deleted from the input string using 94L32+ç (Try here). We join this string using J and remove [a-zA-Z0-9_] which is stored in žj (Try here). We remove all the characters that are in the second string from the first string, which will leave us:

!"#$%&'()*+,-./:;<=>?@[\]^`{|}~

That can also be tested here. We Duplicate this and store in to X with the U-command. We then remove all the characters that are in this string from the input. We then split on whitespaces using ð¡ and remove all empty strings (using ""K). We now have this.

This is the clean version of the input, which we will work with. We map over each element using v. This uses y as the string variable. We take the first character of the string using ¬ and push X, which contains a string with all forbidden characters (!"#$%&'()*+,-./:;<=>?@[\]^`{|}~). We check if the lowercase version of the first character, (which will also be ©opied to the register), is in this string using å. Covered by this part: ï>i, if the first letter doesn't exist in the string of forbidden characters (X), we append this letter to the list of forbidden characters (done with X®«U) and we push y on top of the stack.

Finally, when the strings are filtered, we join the stack by spaces with ðý.

\$\endgroup\$
  • 1
    \$\begingroup\$ ... explanation? :-) \$\endgroup\$ – Luis Mendo Apr 8 '16 at 9:59
  • \$\begingroup\$ @LuisMendo Thanks for reminding me! Done :) \$\endgroup\$ – Adnan Apr 8 '16 at 11:24
2
\$\begingroup\$

PHP

Inspired by the use of regex in most of the answers, I originally tried to do this without using regex at all just to show off a neat variation, but the sticking point of not having clean strings as input ruined that idea. Sad.

With function wrapper, 89 bytes

function f($s){foreach(preg_split('/\W/',$s)as$w)$c[lcfirst($w)[0]]++?:$v.=" $w";echo$v;}

Without function wrapper (needing $s pre-declared), 73 bytes

foreach(preg_split('/\W/',$s)as$w)$c[lcfirst($w)[0]]++?:$v.=" $w";echo$v;

Explanation:

foreach(preg_split('/\W/',$s)as$w)$c[lcfirst($w)[0]]++?:$v.=" $w";echo$v;
        preg_split('/\w/',$s)                                             Break input on all non-word characters
foreach(                     as$w)                                        Loop through each 'word'
                                     lcfirst($w)[0]                       Take the first letter of the lowercase version of the word
                                  $c[              ]++?:                  Increment an array element with a key of that letter after checking if it's false-y (0)
                                                        $v.=" $w";        Add the word if the letter wasn't found (if the previous condition evaluated to false)
                                                                  echo$v; Print the new string to screen.

My only regret is that I couldn't find a faster way of checking/converting letter case.

\$\endgroup\$
2
\$\begingroup\$

Python, 103 bytes

import re
lambda s,d=[]:[w for w in re.findall("\w+",s)if(d.append(w.lower()[0])or d[-1])not in d[:-1]]
\$\endgroup\$
1
\$\begingroup\$

Lua, 172 Bytes

It ended up way longer that I wanted...

t={}(...):gsub("[%w_]+",function(w)b=nil for i=1,#t
do b=t[i]:sub(1,1):lower()==w:sub(1,1):lower()and 1 or b
end t[#t+1]=not b and w or nil end)print(table.concat(t," "))

Ungolfed

t={}                           -- initialise the accepted words list
(...):gsub("[%w_]+",function(w)-- iterate over each group of alphanumericals and underscores
  b=nil                        -- initialise b (boolean->do we have this letter or not)
  for i=1,#t                   -- iterate over t
  do
    b=t[i]:sub(1,1):lower()    -- compare the first char of t's i word
       ==w:sub(1,1):lower()    -- and the first char of the current word
           and 1               -- if they are equals, set b to 1
           or b                -- else, don't change it
  end
  t[#t+1]=not b and w or nil   -- insert w into t if b isn't set
end)

print(table.concat(t," "))     -- print the content of t separated by spaces
\$\endgroup\$
1
\$\begingroup\$

Seriously, 43 bytes

6╙¬▀'_+,;)-@s`;0@Eùk`M┬i;╗;lrZ`i@╜í=`M@░' j

Try it online!

The lack of regex capabilities made this much more difficult than it needed to be.

Explanation:

6╙¬▀'_+,;)-@s`;0@Eùk`M┬i;╗;lrZ`i@╜í=`M@░' j
6╙¬▀                                         push digits in base 62 (uppercase and lowercase letters and numbers)
    '_+                                      prepend underscore
       ,;)                                   push two copies of input, move one to bottom of stack
          -                                  get all characters in input that are not letters, numbers, or underscores
           @s                                split input on all occurrences of non-word characters
             `;0@Eùk`M                       for each word: push the first letter (lowercased)
                      ┬i                     transpose and flatten (TOS is list of first letters, then list of words)
                        ;╗                   push a copy of the first letters list to register 0
                          ;lrZ               zip the list of first letters with their positions in the list
                              `i@╜í=`M       for each first letter: push 1 if that is the first time the letter has been encountered (first index of the letter matches its own index) else 0
                                      @░     filter words (take words where corresponding element in the previous list is truthy)
                                        ' j  join on spaces
\$\endgroup\$
1
\$\begingroup\$

Ruby 76 Bytes

s;f={};s.scan(/(([\w])[\w]*)/).map{|h,i|f[j=i.upcase]?nil:(f[j]=!p; h)}.compact.*' '

Or with method definition 88 bytes

def m s;f={};(s.scan(/((\w)\w*)/).map{|h,i|f[j=i.upcase]?nil:(f[j]=1; h)}-[p]).*' ';end

Ungolfed and with unit test:

def m_long(s)
  #found  - Hash with already found initials
  f={}
  #h=hit, i=initial, j=i[0].downcase
  s.scan(/(([\w\d])[\w\d]*)/).map{|h,i| 
    f[j=i.upcase] ? nil : (f[j] = true; h)
  }.compact.join(' ')
end
#true == !p
#~ def m(s)
  #~ f={};s.scan(/(([\w\d])[\w\d]*)/).map{|h,i|f[j=i.upcase]?nil:(f[j]=!p; h)}.compact.join' '
#~ end
def m s;f={};s.scan(/(([\w\d])[\w\d]*)/).map{|h,i|f[j=i.upcase]?nil:(f[j]=!p; h)}.compact.join' ';end

#~ s = "Ferulas flourish in gorgeous gardens."
#~ p s.split

require 'minitest/autorun'
class FirstLetterTest < Minitest::Test
  def test_1
    assert_equal("Ferulas in gorgeous",m("Ferulas flourish in gorgeous gardens."))
    assert_equal("Ferulas in gorgeous",m_long("Ferulas flourish in gorgeous gardens."))
  end
  def test_2
    assert_equal("Take all first words each letter is",m("Take all first words for each letter... this is a test"))
    assert_equal("Take all first words each letter is",m_long("Take all first words for each letter... this is a test"))
  end
  def test_3
    assert_equal("Look _ There are 3 dogs",m("Look ^_^ .... There are 3 little dogs :)"))
    assert_equal("Look _ There are 3 dogs",m_long("Look ^_^ .... There are 3 little dogs :)"))
  end
  def test_4
    assert_equal("maybe some day 1 plus 2 could result in 3",m("...maybe some day 1 plus 2 plus 20 could result in 3"))
    assert_equal("maybe some day 1 plus 2 could result in 3",m_long("...maybe some day 1 plus 2 plus 20 could result in 3"))
  end
end
\$\endgroup\$
  • \$\begingroup\$ In Regex, \w includes number characters, so [\w\d] can be replaced with \w. Also, if nil values are in an array when you call join' ' (or better yet, *' ' is a shorthand you can use to save more bytes), they vanish, so the call to compact is unnecessary. \$\endgroup\$ – Value Ink Apr 8 '16 at 6:27
  • \$\begingroup\$ @KevinLau Thanks. The \w\dis embarrassing for me. But if I remove the compact I get additional spaces, (see ['x',nil,'x']*'y' == 'xyyx'). Or did I miss something? \$\endgroup\$ – knut Apr 8 '16 at 8:56
  • \$\begingroup\$ Whoops, you're right. In that case, (list-[p]) saves bytes over list.compact. Also, /\w/ is equivalent to /[\w]/. Finally, you can replace your nil with p and your !p with 1 (since your hash only needs truthy values in it) \$\endgroup\$ – Value Ink Apr 8 '16 at 9:08
  • \$\begingroup\$ Thanks, I added your remarks, The replacement of nil with p does not work. If I use it inside my code I get a syntax error. I have to encapsulate like (p) - but then I have again 3 characters. \$\endgroup\$ – knut Apr 8 '16 at 9:56
  • \$\begingroup\$ Flip the ternary and then it works to save a byte: !f[j=i.upcase]?(f[j]=1;h):p. Also just thought of this, but because of string indexing, using s.scan(/\w+/) and removing the i in favor of h[0] works too. \$\endgroup\$ – Value Ink Apr 8 '16 at 21:13
1
\$\begingroup\$

grep and awk, 68 56 bytes

The script:

echo `grep -o '\w*'|awk '!x[tolower(substr($0,1,1))]++'`

Explanation:

  • grep -o matches the legal words, printing each on its own line.

  • awk takes the first letter of each line with substr, makes it lowercase, and then increments a hashtable entry with that key. If the value was unset before the increment, the line is printed.

  • echo ... turns the lines back into words

I previously tried to create a solution without awk, using uniq, sort, grep and bash but fell just short. History in the edits.

Thanks to Dennis for some improvements I missed.

\$\endgroup\$
0
\$\begingroup\$

Python 3.5, 138 bytes:

import re;lambda o,t=[]:''.join([y[0]for y in[(u+' ',t.append(u[0].lower()))for u in re.sub('\W+',' ',o).split()if u[0].lower()not in t]])

Basically, what's happening is..

  1. Using a simple regular expression, the program replaces all the characters, except lowercase or uppercase letters, digits, or underscores in the given string with spaces, and then splits the string at those spaces.
  2. Then, using list comprehension, create a list that iterates through all the words in the split string, and add the first letters of each word to list "t".
  3. In the process, if the current word's first letter is NOT already in the list "t", then that word and a trailing space are added to the current list being created. Otherwise, the list continues on appending the first letters of each word to list "t".
  4. Finally, when all words in the split have been iterated through, the words in the new list are joined into a string and returned.
\$\endgroup\$
0
\$\begingroup\$

PHP 120bytes

function a($s){foreach(preg_split('/\W/',$s)as$w)if(!$o[ucfirst($w[0])]){$o[ucfirst($w[0])]=$w;}return implode(" ",$o);}

This generates a bunch of warnings but that's fine.

\$\endgroup\$
  • \$\begingroup\$ Is the function necessary? \$\endgroup\$ – A.L Apr 7 '16 at 11:33
0
\$\begingroup\$

Javascript ES6, 108 107 chars

107 chars, result string is trimmed

r=s=>s.split``.reverse().join``
f=s=>r(r(s).replace(/\b\w*(\w)\b(?=.*\1\b)/gi,'')).replace(/\W+/g,' ').trim()

Test:

["Take all first words for each letter... this is a test",
"Look ^_^ .... There are 3 little dogs :)",
"...maybe some day 1 plus 2 plus 20 could result in 3"
].map(f) + '' == [
"Take all first words each letter is",
"Look _ There are 3 dogs",
"maybe some day 1 plus 2 could result in 3"
]
\$\endgroup\$
0
\$\begingroup\$

Tcl, 150 bytes

proc F {s D\ {}} {lmap w [split $s] {regsub -all \[^\\w] $w "" f
if {![dict e $D [set k [string tol [string in $f 0]]]]} {dict se D $k $f}}
dict v $D}

Try it online!

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.