Menu Shortcuts
Traditionally, user menus are accessible by keyboard shortcuts, such as Alt + (a letter)
, or even simply hitting the letter when all textboxes are unfocused (gmail style).
Your task
Given the menu entries as an input, your task is to grant each menu entry a proper shortcut letter.
Write a function or a program that accepts a set of words - the menu entries (as an array of strings, or your language equivalent), and returns a dictionary, or a hashmap, from a single letter to a menu entry.
You can either use a parameter and return a value, or use the STDIN and output your results to STDOUT. You are not allowed to assume a global/scope variable is already populated with the input.
Algorithm to determine the proper letter
- Basically it's the first available letter of the word. See assumptions and examples below.
- In case all entry's letters are not available, the shortcut will be
(a letter) + (a number)
. Which letter you pick from the entry is arbitrary. The number should start from 0 and be incremented by 1 - such that all shortcuts are unique. See third example below.
Assumptions
- The input will be a Set, i.e. no repetitions, every entry is unique.
- Length of the input can be any non-negative integer (up to MAX_INT of your language).
- Case sensitivity: Input is case-sensitive, (but will stay unique when ignoring case). The results should contain the original entries with their original casing. However, the output shortcut letters are not case-sensitive.
- All input words will not end with numbers.
- No "evil input" will be tested. "Evil input" is such that you have to increment the counter of a certain letter more than 10 times.
Examples
Examples below are in JSON, but you can use your language equivalent for an array and a Dictionary, or - in case you're using STD I/O - any readable format for your input and output (such as csv, or even space-separated values).
1.
Input: ['File', 'Edit', 'View', 'Help']
Output: {f:'File', e:'Edit', v:'View', h:'Help'}
2.
Input: ['Foo', 'Bar', 'FooBar', 'FooBars']
Output: {f:'Foo', b:'Bar', o:'FooBar', a:'FooBars'}
3.
Input: ['a', 'b', 'aa', 'bb', 'bbq', 'bbb', 'ba']
Output: {a:'a', b:'b', a0:'aa', b0:'bb', q:'bbq', b1:'bbb', b2:'ba'}
Winning conditions
Shortest code wins. Only ASCII is allowed.
6 Answers 6
Javascript (ES6) (削除) 106 (削除ここまで) (削除) 105 (削除ここまで) 100
This function takes input as an an array and outputs a javascript object.
f=i=>i.map(a=>{for(b of c=a.toLowerCase(d=0)+d+123456789)d<!o[e=b>=0?c[0]+b:b]&&(o[d=e]=a)},o={})&&o
Results:
f(['File', 'Edit', 'View', 'Help']);
// {"f":"File","e":"Edit","v":"View","h":"Help"}
f(['Foo', 'Bar', 'FooBar', 'FooBars']);
// {"f":"Foo","b":"Bar","o":"FooBar","a":"FooBars"}
f(['a', 'b', 'aa', 'bb', 'bbq', 'bbb', 'ba']);
// {"a":"a","b":"b","a0":"aa","b0":"bb","q":"bbq","b1":"bbb","b2":"ba"}
Ungolfed / Commented:
f=i=>{
o={}; // initialize an object for output
i.map(a=> // loop through all values in input
for(b of c=a.toLowerCase(d=0)+d+123456789) // loop through all characters of the string with 0123456789 appended to the end
// and initialize d as 0 to be used as a flag
e=b>=0?c[0]+b:b // if b is a number, set e to the first character + the number, otherwise b
if(d<!o[e]) // if the flag hasn't been triggered and o doesn't have a property e
o[d=e]=a // then store the value at e and trigger the d flag
)
return o // return the output object
}
-
\$\begingroup\$ This is beautiful. It may fail for the evil input
['a', 'aa', 'aaa', 'aaaa', 'aaaaa', 'aaaaaa', 'aaaaaaa', 'aaaaaaaa', 'aaaaaaaaa', 'aaaaaaaaaa', 'aaaaaaaaaaa', 'aaaaaaaaaaaa']
, but I think we can ignore such edge cases, can't we? \$\endgroup\$Jacob– Jacob2014年06月02日 13:42:05 +00:00Commented Jun 2, 2014 at 13:42 -
\$\begingroup\$ @Jacob And what happens when we hit
11
? You can't press the one key twice in a keyboard shortcut :P \$\endgroup\$nderscore– nderscore2014年06月02日 14:28:11 +00:00Commented Jun 2, 2014 at 14:28 -
\$\begingroup\$ You have a point there (though it might be possible, given an implementation that waits until end of keystrokes (200ms or so)). Anyhow, I will add to assumptions no such evil input will be tested. \$\endgroup\$Jacob– Jacob2014年06月02日 19:49:14 +00:00Commented Jun 2, 2014 at 19:49
Python 2.x - (削除) 176 170 157 (削除ここまで) 114 bytes
Very simple approach, but someone has to kick the game going.
r={}
for i in input():a=list(i.upper());r[([c for c in a+[a[0]+`x`for x in range(10)]if c not in r])[0]]=i
print r
Edit 1: Reversed the checking operation and made it set the result only once.
Edit 2: Removed branching.
Edit 3: Removed unnecessary dictionary. (thanks to the added assumption)
Examples:
Input: ['File', 'Edit', 'View', 'Help']
Output: {'H': 'Help', 'V': 'View', 'E': 'Edit', 'F': 'File'}
Input: ['Foo', 'Bar', 'FooBar', 'FooBars']
Output: {'A': 'FooBars', 'B': 'Bar', 'O': 'FooBar', 'F': 'Foo'}
Input: ['a', 'b', 'aa', 'bb', 'bbq', 'bbb', 'ba']
Output: {'A': 'a', 'B': 'b', 'Q': 'bbq', 'A0': 'aa', 'B0': 'bb', 'B1': 'bbb', 'B2': 'ba'}
I think the only required explanation is the ungolfed code. (This actually is the original version)
items = input() # ['File', 'Edit', 'View', 'Help']
chars = map(chr,range(65,91))
numbers = {}.fromkeys(chars,0)
result = {}
for item in items:
try:
key = [c for c in item.upper() if c in chars][0] # causes an exception when no items match
result[key] = item
chars.remove(key)
except:
key = item[0].upper()
result[key+`numbers[key]`] = item
numbers[key] += 1
print result
-
\$\begingroup\$ I have to say humble thank you to @Jacob. The input format is just great. \$\endgroup\$seequ– seequ2014年06月01日 17:03:29 +00:00Commented Jun 1, 2014 at 17:03
JavaScript (ECMAScript 6) - 107 Characters
f=a=>(o={},p={},[o[[c for(c of l=w.toLowerCase())if(!o[c])][0]||(k=l[0])+(p[k]=p[k]+1|0)]=w for(w of a)],o)
Explanation:
f=a=>(
o={}, // The dictionary to output
p={}, // Stores record of numbers appended after duplicate
// menu keys
[ // Use array comprehension for each word w of input a
(unmatchedCharacters
=[c // Use array comprehension for each character c of
for(c of l=w.toLowerCase()) // the lower case of word w but only get
if(!o[c]) // those characters which are not already a key in o.
],
key=unmatchedCharacters[0] // Take the first of those characters
|| // Or if all characters are already in o
(k=l[0]) // Take the first character of the lower-case word
+(p[k]=p[k]+1|0), // concatenated with the increment of the digit stored
// in p (or zero).
o[key]=w) // Set o to map from this key to the word
for(w of a)
],
o) // return o
Tests:
f(['File', 'Edit', 'View', 'Help']);
{f: "File", e: "Edit", v: "View", h: "Help"}
f(['Foo', 'Bar', 'FooBar', 'FooBars']);
{f: "Foo", b: "Bar", o: "FooBar", a: "FooBars"}
f(['a', 'b', 'aa', 'bb', 'bbq', 'bbb', 'ba']);
{a: "a", b: "b", a0: "aa", b0: "bb", q: "bbq", b1: "bbb", b2: "ba"}
PHP>= 5.4 - 149 characters
According to PHP's standards (insert sniggers here), the input isn't valid JSON as it uses '
instead of "
, so I've been a bit cheeky and I'm using the Input as an actual variable declaration:
<?
$i = ['a', 'b', 'aa', 'bb', 'bbq', 'bbb', 'ba'];
$c=[];foreach($i as$w){foreach(str_split($w) as$j)if(!$c[$j]){$x=$j;goto f;}$n=0;do{$x=$w[0].$n++;}while($c[$x]);f:$c[$x]=$w;}echo json_encode($c);
Using the examples:
Input: ['File', 'Edit', 'View', 'Help']
Output: {"F":"File","E":"Edit","V":"View","H":"Help"}
Input: ['Foo', 'Bar', 'FooBar', 'FooBars']
Output: {"F":"Foo","B":"Bar","o":"FooBar","a":"FooBars"}
Input: ['a', 'b', 'aa', 'bb', 'bbq', 'bbb', 'ba']
Output: {"a":"a","b":"b","a0":"aa","b0":"bb","q":"bbq","b1":"bbb","b2":"ba"}
Un-golfified it's pretty basic:
<?
$i = ['a', 'b', 'aa', 'bb', 'bbq', 'bbb', 'ba'];
$c = [];
foreach($i as $w)
{
foreach(str_split($w) as $j)
if(!$c[$j])
{
$x = $j;
goto f;
}
$n = 0;
do
{
$x = $w[0] . $n++;
}
while($c[$x]);
f: $c[$x] = $w;
}
echo json_encode($c);
-
\$\begingroup\$ PHP has jump declarations? That's so... 90's. \$\endgroup\$seequ– seequ2014年06月01日 22:21:38 +00:00Commented Jun 1, 2014 at 22:21
-
2\$\begingroup\$ You don't have to stick to JSON, I only provided the examples in JSON, but, as stated in the question, you may choose any readable format for output or use your language equivalent for a Dictionary. (You can save 13 characters by removing the
json_encode
invocation). \$\endgroup\$Jacob– Jacob2014年06月02日 09:10:53 +00:00Commented Jun 2, 2014 at 9:10 -
\$\begingroup\$
echo
doesn´t work with arrays; butprint_r($c);
would do it, saving 9 bytes. \$\endgroup\$Titus– Titus2016年09月14日 21:03:42 +00:00Commented Sep 14, 2016 at 21:03 -
\$\begingroup\$ But this isn´t case insensitive.
str_split(strtoupper($w))
anducfirst($w[0])
can solve that (+21); or$s=strtoupper($w);
(+18) \$\endgroup\$Titus– Titus2016年09月14日 21:11:57 +00:00Commented Sep 14, 2016 at 21:11
PowerShell, (削除) 91 (削除ここまで) 83 bytes
$r=@{}
$args|%{$r[($_|% *wer|% t*y|%{$c=$_;,''+0..9|%{$c+$_}|?{!$r.$_}})[0]]=$_}
$r
It throws an exception if a proper shortcut not found.
Unrolled:
$result=@{}
$args|%{
$shortcuts = $_|% toLower|% toCharArray|%{
$c=$_
,''+0..9|%{$c+$_}|?{!$result.$_} # output shortcuts are not exist in the result
}
$properShortcut = $shortcuts[0] # throws an exception if a proper shortcut not found
$result[$properShortcut]=$_
}
$result
PHP, 153 bytes
for($c=[];$w=trim(fgets(STDIN));$c[reset(array_diff(str_split($s),array_keys($c)))?:$y]=$w){$s=strtoupper($w);for($n=0;$c[$y=$s[0].$n++];);}print_r($c);
run with php-r '<code>' <<EOF
+ Enter + <word1>
+Enter + <word2>
+Enter + ... + EOF
+Enter
working on argv for 155 bytes:
$c=[];foreach($argv as$i=>$w)if($i){$s=strtoupper($w);for($n=0;$c[$y=$s[0].$n++];);$c[reset(array_diff(str_split($s),array_keys($c)))?:$y]=$w;}print_r($c);
run with php -r '<code>' <word1> <word2> ...
(-13 bytes with a defined global: foreach($i as$w)
instead of foreach($argv as$i=>$w)if($i)
)
['ab', 'a']
give{a:'ab', a0:'a'}
or{b:'ab', a:'a'}
? \$\endgroup\$