# Automatically anti-predictably assemble an alliterative aria

Thanks to @ComradeSparklePony for the title.

This challenge should be very simple. You are given three lists.

The first is a list of first names, in title case.

The second is a list of adjectives, in lower case.

The third is a list of nouns, in lower case.

Please randomly select a name, optional adjective, and noun, and output <Name>'s <adjective> <noun>. However, each word must begin with the same letter. You can assume that all words begin with a letter. You can also assume (but note in your answer if you do):

• that all words are composed solely of alphabetic characters
• that there is at least one noun for each name
• that there is at least one name for each noun

You cannot however assume that an adjective exists for a particular pair of name and noun, as the adjective is optional so the output will still be valid.

You do not have to select the shared letter uniformly, although all available letters must have a non-zero chance of occurring. You must however ensure that all outputs for a given letter have as near equal chance of occurring as possible within the limits of your language's random number generator. In the case of the adjective, this is equivalent to having an extra entry meaning "no adjective for this letter" which has the same chance as all of the other adjectives for that letter.

Example input lists:

Joan Neil Nicola Oswald Sherman Stephanie
new novel old original second silent
jeep noun novel output second sheep snake


Example outputs for these inputs (each line is a separate example):

Stephanie's second second
Sherman's silent snake
Oswald's original output
Nicola's novel novel
Neil's noun
Joan's jeep


Note no extra space between words in the last two examples.

This is , so the shortest code that breaks no standard loopholes wins!

In the unlikely event that it helps, you can input everything in upper case, but you still need to output in sentence case.

• Are we correct to assume that the program should return: 1 name 1 adjective (if one matches the name) 1 noun ? Or are you asking to produce an output for each name? – DavidC May 12 at 11:26
• Maybe you should add 'Joan' and 'jeep' in your example to illustrate the fact that there might be no adjective at all for a given letter? – Arnauld May 12 at 11:55
• Given your example input is the chance of no adjective 1 in 3 (since all adjective "lists" are 2 long)? ...and if 'Joan' and 'Jeep' were also there with no j-adjective would the chance become 4 in 9? Might be worth placing probabilities against outputs, or enumerating all outputs -- as I understand it not only "all outputs for a given letter..." but also all distinct outputs should have equal likelihood (given distinct values within each list). – Jonathan Allan May 12 at 12:37
• @DavidC Sorry, I realise adding extra examples has made that unclear; you only produce one line of output for each invocation. – Neil May 12 at 13:16
• @JonathanAllan Adding "Joan" and "jeep" wouldn't affect the relative chances of "Neil's noun" being output compared with other options containing "Neil" and "noun". – Neil May 12 at 13:17

# Jelly,  27 25  24 bytes

-1 thanks to Erik the Outgolfer (use a zero instead of a space character)

Ż€2¦Œpḟ€0ZḢŒuEƲƇXż“'s“”K


A full program accepting an argument in the form of a Python formatted list of lists of strings which prints the output to STDOUTt.

Try it online!

### How?

Ż€2¦Œpḟ€0ZḢŒuEƲƇXż“'s“”K - Main Link: list of lists of lists of characters
€ ¦                     - sparse application...
2                      - ...to indices: [2]
Ż                        - ...action: prepend a zero (place holder for no adjective)
Œp                   - Cartesian product (all choices, including invalid ones)
€                 - for each:
ḟ 0                -   filter out any zeros
Ƈ         - filter keep those for which:
Z               -     transpose
Œu            -     upper-case
E           -     all equal?
X        - random (uniform) choice  e.g. [['B','o','b'],['b','l','u','e'],['b','a','g']]
ż       - zip with:
“'s“”  -   list [["'", 's'], []]       [[['B','o','b'],["'", 's']],[['b','l','u','e'],[]],['b','a','g']]
K - join with spaces              [['B','o','b'],["'", 's'],' ',['b','l','u','e'],[],' ','b','a','g']
- implicit (smashing) print     Bob's blue bag


# 05AB1E,  24 23  21 bytes

Assumes there is a noun for each name, as allowed by the challenge.

„'s«I¯ªâIâ€˜ʒl€нË}Ωðý


Try it online!

Explanation

„'s«                    # append "'s" to all names in the name-list
I¯ª                 # append an empty list to the adjective-list
â                # cartesian product between the lists
Iâ              # cartesian product with the noun-list
€˜            # deep flatten each sublist
ʒ    }      # filter, keep only lists that when
l          # converted to lowercase
€н        # with only heads kept
Ë       # have all elements equal
Ω     # pick a valid list uniformly at random
ðý   # and join by spaces

• Oh, the ¯ª and €˜ are smart! I had a 26 byte answer, but was having trouble fixing the double-space when there isn't an adjective.. – Kevin Cruijssen May 13 at 9:38
• @KevinCruijssen: Yeah that was the part I had the most issues with as well. Took me a while to realize that I could use ¯ instead of filling with empty strings I had to manually clean up later. – Emigna May 13 at 9:40

# R, 155 148 bytes

-7 bytes thanks to Giuseppe (using * for sample)

function(x,y,z){*=sample
while(T)T=length(unique(c(tolower(substr(c(a<-x*1,b<-c(y,"")*1,c<-z*1),1,1)),"")))-2
paste0(a,"'s ",b,if(nchar(b))" ",c)}


Try it online!

Uses rejection sampling: draw at random a name, an adjective (possibly the empty string) and a noun until the first letters match. This condition is checked by counting if the number of unique elements in the vector formed of the first letters, plus the empty string, is of length 2 - this allows for an empty adjective.

Then print the result, with an extra space if the adjective is non-empty.

The different possibilities starting with the same letter have equal occurrence probabilities, since sample draws from the uniform distribution. The easiest way to see this is to condition on the event that the name and noun start with the same letter (which is fine: if they don't, we would reject). Now condition on the event that we accept: this means we draw either the empty adjective, or an adjective starting with the same letter. Each of these possibilities still has equal probability.

Check the probabilities on $$\10^5\$$ replicates.

• Does this have an equal chance of an empty adjective to each other possibility for any given first letter? – Nick Kennedy May 12 at 19:46
• @NickKennedy Yes, since sample draws from the uniform distribution. The easiest way to see this is to condition on the event that the name and noun start with the same letter (which is fine: if they don't, we would reject). Now condition on the event that we accept: this means we draw either the empty adjective, or an adjective starting with the same letter. Each of these possibilities still has equal probability. – Robin Ryder May 12 at 19:57
• thanks, well explained. – Nick Kennedy May 12 at 19:57
• @NickKennedy Thanks, I'll add that explanation to the post along with a link to verify empirically that the probabilities are equal. – Robin Ryder May 12 at 20:20
• 148 bytes – Giuseppe May 13 at 14:20

# JavaScript (ES6),  139 124 122  120 bytes

Save 2 bytes thanks to @Neil

Takes input as (names,adjectives)(nouns).

(N,a)=>F=n=>/^(.)\S+( \1\S+)+$/i.test(s=(g=a=>a[Math.random()*a.length|0])(N)+"'s "+[(o=g([,...a]))&&o+' ']+g(n))?s:F(n)  Try it online! Or check the distribution on 5 million draws ### How? The helper function $$\g\$$ takes an array and returns a random element from this array, with a uniform distribution. g = a => a[Math.random() * a.length | 0]  By invoking $$\g\$$ three times, we generate a random string $$\s\$$ with a valid format, but without taking the initial letters into account. For the adjective, we append an empty entry and make sure not to insert a trailing space if it's chosen. s = g(N) + "'s " + [(o = g([, ...a])) && o + ' '] + g(n)  We then check if all initial letters are identical with the following regular expression: /^(.)\S+( \1\S+)+$/i


It not, we simply try again until $$\s\$$ is valid.

• +[(o=g([,...a]))&&o+' ']+ saves 2 bytes, I think? – Neil May 12 at 13:37
• @Neil Ah, yes. Nice one. – Arnauld May 12 at 13:44

# Python 3, 161 154 151 147 145 bytes

(Thanks ArBo, EmbodimentOfIgnorance, Neil who have contributed 2, 3 and 4 bytes to my first golf!)

from random import*
c=choice
def f(N,a,n):
s=c(N);w=s[0].lower();o=N
while o[0]!=w:o=c(n)
print(s+"'s",c([x+" "for x in a if x[0]==w]+[""])+o)


Try it online! (with 500k executions)

• Takes three lists as inputs.

• Assumes at least one noun for each name.

Same score, more golf-y:

# Python 3, 145 bytes

from random import*
c=choice
def f(N,a,n):
s=c(N);y=lambda p,e=[]:c([x+" "for x in p if x[0]==s[0].lower()]+e);print(s+"'s",y(a,[""])+y(n)[:-1])


Try it online! (with 500k executions)

It's just 140 if trailing whitespaces are allowed (by removing square face [:-1])

• Nice first answer! You can save a byte in the first while loop: while t>""<t[0]!=w. You can also replace the last line by print(s+"'s",t+(t and" ")+o), dropping the u= in the third line. – ArBo May 12 at 13:50
• I ended up changing my solution because the previous didn't fit the requirements – Nicola Sap May 12 at 15:20
• 152 bytes (Footer removed to fit URL in comment) – Embodiment of Ignorance May 12 at 18:08
• You're only using the variable t once so you can save 4 bytes by inlining the code. I think you can switch o to use a similar code pattern to t, and then save another 4 bytes by inlining that too. – Neil May 13 at 10:27
• Thanks, you guys are really helping! @Neil, I haven't be able to refactor o: I reach this: from random import* c=choice def f(N,a,n): s=c(N);y=lambda p,e=[]:c([x for x in p if x[0]==s[0].lower()]+e);print(s+"'s",y(a,[""])+y(n)) (137) but adding the conditional whitespace, via an optional arg to y, costs me 11 bytes – Nicola Sap May 13 at 12:56

# Jelly, 28 bytes

1ịZḢXɓŒuḢ=ɗƇ€Ż€2¦X€ḟ0ż“'s“”K


Try it online!

Wrote this before I saw @JonathanAllan’s shorter answer, but thought it worth posting since it uses a different approach. Saved 3 bytes by @EriktheOutgolfer’s suggestion on that answer.

A full program taking a list of lists of strings and implicitly printing a randomly selected alliteration. Assumes at least one noun per name.

# C# (Visual C# Interactive Compiler), 176 bytes

(a,b,c)=>(a=a[z.Next(a.Count)])+"'s "+b.Where(x=>(x[0]&95)==a[0]).Append("").OrderBy(x=>z.Next()).Last()+" "+c.OrderBy(x=>z.Next()).Last(x=>(x[0]&95)==a[0]);var z=new Random();


Try it online!

• You can assume names begin with an uppercase letter, so you can just uppercase the other letters for the comparison, which should save you 10 bytes? – Neil May 12 at 23:19
• @Neil Yep, exactly 10 bytes :) – Embodiment of Ignorance May 13 at 1:30

# Red, 179 bytes

func[a b c][random a random c
foreach k c[if k/1 = h: a/1/1 + 32[g: rejoin[sp k]]]collect/into[foreach
d b[if d/1 = h[keep rejoin[sp d]]]]e: copy[""]random e rejoin[a/1"'s"e/1 g]]


Try it online!

## Explanation:

Red[]
f: func[a b c][                     ; a function with 3 arguments
random a                        ; shuffle the list of names in place
random c                        ; shuffle the list of nouns in place
foreach k c [                   ; for each item in the shuffled list of nouns
if k/1 = h: a/1/1 + 32 [    ; check if it begins with the same lowercase letter
; as the first name in the shuffled list of names
g: rejoin [" " k]       ; if yes, then insert a " " in front of it save it as g
]                           ; thus I always get the last match
]
collect/into [                  ; collect in a new list e
foreach d b [               ; all items form the adjectives list
if d/1 = h [            ; that start with the same lowercase letter as the 1st noun
keep rejoin [" " d] ; insert a " " in form of the adjective
]
]
] e: copy[""]                   ; the list initially has a single item - the empty string
random e                         ; shuffle the extracted adjectives list
rejoin [a/1 "'s" e/1 g]          ; return the formatted string
]


# Scala, 234226234 206 bytes

-28 due to the fact I thought it had to accept StdIn, it's a function now

def f(a:List[String],b:List[String],c:List[String])=scala.util.Random.shuffle(for(d<-a;e<-("" +: b);g<-c;if(d.head.toLower==g.head&&(e.isEmpty||e.head==g.head))) yield s"$d's$e $g".replace(" ", " ")).head  Try it online! Ungolfed: def f(names: List[String], adjectives: List[String], nouns: List[String]) = { val allPossible = for { name <- names adjective <- ("" +: adjectives) // Add the choice of no adjective noun <- nouns if (name.head.toLower == noun.head && (adjective.isEmpty || adjective.head == noun.head)) // Filter out so only matching entries remain } yield s"$name's $adjective$noun"
.replace("  ", " ") // Get rid of artifact created by the empty adjective selection

scala.util.Random.shuffle(allPossible.toList).head // Get a random element
}


# Ruby, 94 bytes

->a,b,c{"#{n=a.sample}'s #{s=[p,*b.grep(r=/^#{n[0]}/i)].sample;s+" "if s}#{c.grep(r).sample}"}


Try it online!

# Icon, 167 163 bytes

procedure f(a,b,c)
!a:=:?a&\x;!c:=:?c&\x;d:=[""]
e:=!b&e[1]==(t:=char(32+ord(a[1,1])))&put(d," "||e)&\x
!d:=:?d&\x;return(!a||"'s"||!d||" "||(k:=!c&t==k[1]&k))
end


Try it online!

Uses the same algorithm as my Red answer.