Automatically anti-predictably assemble an alliterative aria

15

1

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.

Neil

Posted 2019-05-12T10:44:06.703

Reputation: 95 035

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 – 2019-05-12T11:26:44.127

1Maybe 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 – 2019-05-12T11:55:14.370

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 – 2019-05-12T12:37:11.427

@DavidC Sorry, I realise adding extra examples has made that unclear; you only produce one line of output for each invocation. – Neil – 2019-05-12T13:16:29.833

1@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 – 2019-05-12T13:17:25.057

@JonathanAllan As it happens, the example does indeed have a chance of no adjective of 1 in 3, although I hadn't intended it that way. Adding "Joan" and "jeep" would alter the chance of no adjective depending on how frequently the letter "j" was chosen, so it's not necessarily 4 in 9. – Neil – 2019-05-12T13:25:35.067

I was talking about the chance of any output with no adjective. – Jonathan Allan – 2019-05-12T13:26:15.737

"so it's not necessarily 4 in 9" ...so we don't have to be uniform in our choices of name? – Jonathan Allan – 2019-05-12T13:27:35.137

@JonathanAllan You only have to be uniform in your choice between different names that start with the same letter. You don't have to be uniform in your choice of letter. – Neil – 2019-05-12T13:33:03.570

Right, I see (and now see it in the text), thanks for the clarification! – Jonathan Allan – 2019-05-12T13:34:46.870

Are we allowed an extra space between the words if there is no adjective? – Nick Kennedy – 2019-05-12T14:20:27.650

@NickKennedy No, sorry if that wasn't clear from the example. – Neil – 2019-05-12T14:31:49.960

Answers

5

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:
              Ʋ          -   last four links as a monad:
         Z               -     transpose
          Ḣ              -     head
           Œ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

Jonathan Allan

Posted 2019-05-12T10:44:06.703

Reputation: 67 804

24 bytes. – Erik the Outgolfer – 2019-05-12T14:03:40.077

Ah yeah, nice :) – Jonathan Allan – 2019-05-12T14:05:56.517

5

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

Emigna

Posted 2019-05-12T10:44:06.703

Reputation: 50 798

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 – 2019-05-13T09:38:36.900

@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 – 2019-05-13T09:40:57.470

4

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.

Robin Ryder

Posted 2019-05-12T10:44:06.703

Reputation: 6 625

Does this have an equal chance of an empty adjective to each other possibility for any given first letter? – Nick Kennedy – 2019-05-12T19:46:21.887

@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 – 2019-05-12T19:57:05.960

thanks, well explained. – Nick Kennedy – 2019-05-12T19:57:58.360

@NickKennedy Thanks, I'll add that explanation to the post along with a link to verify empirically that the probabilities are equal. – Robin Ryder – 2019-05-12T20:20:14.637

1148 bytes – Giuseppe – 2019-05-13T14:20:20.870

@Giuseppe Thanks! – Robin Ryder – 2019-05-13T16:51:49.900

3

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.

Arnauld

Posted 2019-05-12T10:44:06.703

Reputation: 111 334

+[(o=g([,...a]))&&o+' ']+ saves 2 bytes, I think? – Neil – 2019-05-12T13:37:06.217

@Neil Ah, yes. Nice one. – Arnauld – 2019-05-12T13:44:19.433

3

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])

Nicola Sap

Posted 2019-05-12T10:44:06.703

Reputation: 2 291

1Nice 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 – 2019-05-12T13:50:34.143

I ended up changing my solution because the previous didn't fit the requirements – Nicola Sap – 2019-05-12T15:20:02.603

1152 bytes (Footer removed to fit URL in comment) – Embodiment of Ignorance – 2019-05-12T18:08:37.187

1You'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 – 2019-05-13T10:27:48.703

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 – 2019-05-13T12:56:11.940

Somehow made it :) – Nicola Sap – 2019-05-13T13:21:40.447

You don't need to indent the function body in your second version :) – ArBo – 2019-05-13T16:40:30.720

And you can save one more byte by switching to *args notation in your lambda: 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]) – ArBo – 2019-05-13T16:49:24.440

I think I had def f(N,a,n):s=c(N);w=s[0].lower();print(s+"'s",c([x+" "for x in a if x[0]==w]+[""])+c([x for x in a if x[0]==w])). – Neil – 2019-05-13T17:30:34.337

at what point accepting suggestions becomes straight up cheating? :D – Nicola Sap – 2019-05-13T17:50:07.037

I think removing the [:-1] results in an even better answer: 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)) – Neil – 2019-05-13T18:38:03.957

0

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.

Nick Kennedy

Posted 2019-05-12T10:44:06.703

Reputation: 11 829

0

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!

Embodiment of Ignorance

Posted 2019-05-12T10:44:06.703

Reputation: 7 014

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 – 2019-05-12T23:19:25.050

@Neil Yep, exactly 10 bytes :) – Embodiment of Ignorance – 2019-05-13T01:30:19.970

0

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
]

Galen Ivanov

Posted 2019-05-12T10:44:06.703

Reputation: 13 815

0

Scala, 234 226 234 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
}

Soren

Posted 2019-05-12T10:44:06.703

Reputation: 285

0

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.

Galen Ivanov

Posted 2019-05-12T10:44:06.703

Reputation: 13 815

0

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!

Value Ink

Posted 2019-05-12T10:44:06.703

Reputation: 10 608