1 /**
2 IKEA Name Generator
3 
4 Based on code by Kate McFaul:
5 https://github.com/craftykate/ikea-name-generator
6 */
7 module namegen.ikea;
8 
9 string generateName()
10 {
11     import std.conv: to;
12 
13     return generateNameWide.to!string;
14 }
15 
16 wchar[] generateNameWide()
17 {
18     wchar[] word;
19 
20     ubyte minLength = 3;
21     ubyte maxLength = 10;
22     const ubyte wordLength = uniform(minLength, maxLength, rndg);
23 
24     for (ubyte i = 0; i < wordLength; i++)
25         word ~= returnNextLetter(word);
26 
27     // Checks if word ended with two consonants. For readability, end with an extra vowel
28     word.endWord;
29 
30     // Format name to add Swedish characters if possible
31     word.formatSwedish;
32 
33     import std.string: capitalize;
34 
35     return word.capitalize;
36 }
37 
38 unittest
39 {
40     import std.stdio;
41 
42     foreach(i; 0 .. 13)
43     {
44         auto s = generateName;
45 
46         assert(i != 0 || s == "Åökpe", s);
47         assert(i != 5 || s == "Öålih", s);
48         assert(i != 12 || s == "Tigra", s);
49     }
50 
51     // It is to reset RNDG to initial value
52     rndg = Random(42);
53 }
54 
55 private:
56 
57 static import alphabet = namegen.ikea.alphabet;
58 import std.random;
59 
60 // Seed a random generator with a constant
61 auto rndg = Random(42);
62 
63 // Main function to get and return the next letter
64 char returnNextLetter(in wchar[] word)
65 {
66     static ubyte numConsonants;
67     static ubyte numVowels;
68 
69     char nextLetter;
70 
71     // If it's the first letter, grab any letter
72     if (word.length == 0)
73     {
74         nextLetter = grabAnyLetter();
75 
76     }
77     // If it's not the first letter...
78     // And there are too many consonants before it, grab a vowel
79     // Or it's the second letter and the first wasn't a vowel (this makes sure there's a vowel in the first two letters for readability)
80     else if (numConsonants == 2 || (word.length == 1 && numConsonants == 1))
81     {
82         nextLetter = grabAVowel();
83     }
84     // Or if there are too many vowels grab a consonant
85     else if (numVowels == 2)
86     {
87         nextLetter = grabAConsonant();
88         numConsonants++;
89         numVowels = 0;
90     }
91     else // Otherwise, grab the next acceptable letter
92     {
93         nextLetter = grabNextGoodLetter(word);
94         numVowels++;
95         numConsonants=0;
96     }
97 
98     return nextLetter;
99 }
100 
101 /// Get random letter from string of letters
102 char grabRandomLetter(in string str)
103 {
104     auto idx = uniform(0, str.length, rndg);
105 
106     return str[idx];
107 }
108 
109 /// Get any letter from alphabet
110 char grabAnyLetter()
111 {
112     return alphabet.whole.grabRandomLetter;
113 }
114 
115 char grabAVowel()
116 {
117     return alphabet.vowels.grabRandomLetter;
118 }
119 
120 char grabAConsonant()
121 {
122     return alphabet.consonants.grabRandomLetter;
123 }
124 
125 /// Get a letter than can follow the last letter
126 char grabNextGoodLetter(in wchar[] word)
127 {
128     const lastLetter = word[$-1];
129 
130     return alphabet.after[lastLetter].grabRandomLetter;
131 }
132 
133 import std.algorithm.searching: canFind;
134 
135 bool isVowel(wchar c)
136 {
137     return alphabet.vowels.canFind(c);
138 }
139 
140 // For readability, make sure word has a vowel in the last two letters
141 void endWord(ref wchar[] word)
142 {
143     if(!word[$-2].isVowel && !word[$-1].isVowel)
144         word ~= grabAVowel();
145 }
146 
147 // Change first a and o (if present) to swedish characters
148 void formatSwedish(ref wchar[] word)
149 {
150     // 50/50 chance of changing first 'a'
151     if(dice(rndg, 50, 50) == 0)
152         word.replaceFirst('a', 'å');
153 
154     // 50/50 chance of changing first 'o'
155     if(dice(rndg, 50, 50) == 0)
156         word.replaceFirst('o', 'ö');
157 }
158 
159 void replaceFirst(ref wchar[] word, char from, wchar to)
160 {
161     auto idx = canFind(word, from);
162     word[idx] = to;
163 }