脱・lipsum、自然な英文っぽいダミーテキストを生成してみる

著名なダミーテキストの大家と言えば lorem ipsum だ。しかしながら、モックの開発やテストの時にいちいちコピーしてきたり、ジェネレータ使って作成したりするのがダルくなったので、気分転換も兼ねてランダムな文字列でダミーテキストを生成するシンプルなメソッドを作ってみた。

```typescript
function generateRandomText(mumPhrases: number, phraseLength: number): string {
  const alphabet = 'abcdefghijklmnopqrstuvwxyz'

  function getRandomChar(): string {
    return alphabet.charAt(Math.floor(Math.random() * alphabet.length))
  }

  function getRandomPhrase(length: number): string {
    let phrase = ''
    for (let i = 0; i < length; i++) {
      phrase += getRandomChar()
    }
    return phrase
  }

  let result = ''
  for (let i = 0; i < mumPhrases; i++) {
    result += getRandomPhrase(phraseLength) + ' '
  }

  result = result.trim()

  if (result.length > 0) {
    result = result.charAt(0).toUpperCase() + result.slice(1)
  }

  return result + '.'
}
```

これは`phraseLength`の文字数分を一単語として、`mumPhrases`個分の単語を並べたテキストを作るシンプルなメソッドだ。できた文字列の集合について最後に先頭の文字を大文字に、末尾に`.`を付与して英文っぽいフレーズに加工してから出力している。早速、このメソッドでダミーテキストを生成してみた。

```js
generateRandomText(20, 10)
```

> Zsyimzemva sneoyntord bfhgxyednf gbyuyucfod hyfouxpbii prexciprth fjejxhqdnw ydjcvfxpyn raojvaxbxp lduwssdfbl rjmjkausmx ilrupmbsyk pdhwnlfjkd pkvglrndoj kggdxkvmxq meaoncvwft avzryaafbr smmppjvqdt qzgljmzexz rlvzobmnrg.

何かパスワードの候補がたくさん表示されたw

**こんなダミーテキストじゃ納得できん！**

──と言うことで、もっと自然な英文っぽいダミーテキストを生成するメソッドを作ってみた（本筋の開発を進めなよ、lorem ipsum使えばええやん……という心の声を封殺しつつ）。

まず早速出来上がったメソッドをご紹介。

```typescript
/**
 * Generate Dummy Text
 * @param {number} maxPhrases 
 * @param {number} phraseLength 
 * @param {string | undefined} lineBreak 
 * @returns {string}
 */
function generateDummyText(maxPhrases: number, phraseLength: number, lineBreak?: string): string {
  // Order based on frequency of alphabetical characters used in English phrases.
  const alphabets = 'etaoinshrdlcumwfgypbvkjxqz'
  const alphabet = [...alphabets].map((char, idx) => char.repeat(alphabets.length - idx)).join('')

  // Closures
  const getRandomArbitrary = (min: number, max: number): number => {
    return Math.random() * (max - min) + min
  }
  const weightedRandomChoice = (choices: number[], weights: number[], k: number = 1): number[] | number => {
    const result = []
    const cumulativeWeights = []
    let sum = 0
  
    for (let weight of weights) {
      sum += weight
      cumulativeWeights.push(sum)
    }
  
    for (let i = 0; i < k; i++) {
      const rand = Math.random() * sum
      for (let j = 0; j < cumulativeWeights.length; j++) {
        if (rand < cumulativeWeights[j]) {
          result.push(choices[j])
          break
        }
      }
    }
  
    return result.length === 1 ? result[0] : result
  }
  const getRandomChar = (): string => {
    return alphabet.charAt(Math.floor(Math.random() * alphabet.length))
  }
  const getRandomPhrase = (length: number): string => {
    let phrase = ''
    for (let i = 0; i < length; i++) {
      phrase += getRandomChar()
    }
    return phrase
  }
  
  maxPhrases = Math.ceil(getRandomArbitrary(Math.floor(Math.abs(Math.sin(getRandomArbitrary(1, phraseLength) + 1) * maxPhrases)), maxPhrases))

  const choicesPhraseLength = Array.from({ length: phraseLength }, (_, index) => index + 1)
  const weightedValue = 5.2// Average number of letters in an English word (source by Google Books Ngram).
  const weightedPhraseLength = choicesPhraseLength.map(x => Math.floor((1 / Math.abs(x - weightedValue)) * 10))

  const choicesWords = Array.from({ length: 20 }, (_, index) => index + 1)
  const averageWords = 17// Average number of words in a typical English sentence (source by Google Books Ngram).
  const weightedWords = choicesWords.map(x => {
    const w = Math.floor((1 / Math.abs(x - averageWords)) * 10)
    return w > 10 ? 20 : w
  })

  let phrases = []
  let phrase = ''
  let result = ''
  let separater = ''
  let paragraphs = 0
  let sentences = 0
  let words = 0
  for (let i = 0; i < maxPhrases; i++) {
    if (words > (weightedRandomChoice(choicesWords.slice(7), weightedWords.slice(7), 1) as number)) {
      separater = '.'
    } else {
      separater = ' '
    }
    phrase = getRandomPhrase(weightedRandomChoice(choicesPhraseLength, weightedPhraseLength, 1) as number)
    if (separater === '.') {
      result += phrase + separater
      result = result.trim()
      if (result.length > 0) {
        phrases.push(result.charAt(0).toUpperCase() + result.slice(1))
      }
      result = ''
      sentences++
      words = 0
    } else {
      if (phrase.length === 1) {
        // A one-letter phrase is joined to the next phrase with an apostrophe.
        result += phrase + "'"
      } else {
        result += phrase + separater
        words++
      }
    }
    if (sentences >= getRandomArbitrary(3, 5)) {
      phrases.push("\n")
      paragraphs++
      sentences = 0
    }
  }

  result = result.trim()
  if (result.length > 0) {
    phrase = result.charAt(0).toUpperCase() + result.slice(1)
    // If the sentence ends with an apostrophe, "s" is forcibly added.
    phrase += /'$/.test(phrase) ? 's' : ''
    phrases.push(phrase + '.')
  }

  result = phrases.join(' ')

  if (!!lineBreak) {
    result = result.replace(/\n/g, lineBreak)
  }

  return result
}
```

使い方としては、第1引数に最大フレーズ数、第2引数に1フレーズの最大文字数、第3引数は省略可能で改行コードを変換する時に返還後の文字やタグを指定する。
では、このメソッドを使ってダミーテキストを生成してみよう。

```js
generateDummyText(200, 10, '<br>')
```

> Fgroaa liosal ai puco lwooi dldga hruel xrlrauo geegeugufu arba ownjffajes sllli uuua saeyd esatv dvvfm ip. Omgwj yylel sdup ngige yettizht hteue wblo doliubm lwyquus owrai uchkt aihbue. Ettms wihlde hen dcroo lphef yldel wbormrw cdtod ikptx nltsw hkash roiunsl ufecs. Rs dmnkgo kpic hlywd tffmpoca wghtu rdsad tecri atepn movdt eleprw mwed hdfuwhogax xnhtno wie cya ipnnoo.
Ttlncmn fysvu hicji awrr iltim os sfhe glodt trtki wrsnh ynpa rslgtmc oo a'crerca. Yuhqsm inuar linmm haxli bhdcml nfkio iyotk ftsto lbuhwhwtu vttad voefa asbgr facsr wsuii sngoo iohn aufrzloao. Tweit ewime muoon enldy tkerj maehr inilue ddnpt saoog hutsv aynag sruenocdtg miowutnoso ayfmf raafp afxvv ytlcd wimlt. Etccv dolle ovfur viypfp dflrd oaa emode uatott sftak wwgay aobmotoeye lsstf eas dcino lie dhoee c'gsebawnum bdtwm ihrvpoa.
Diwki vozei xbnic rmtkovt oosoi haglowrhtm nttaf mnluw udhcm osam lu hctscintp rrmtw iuhn jehac ii fjmdb pumcya. Uedar ancee numodt cnadi iesvl nlaaloj ppdag aonnx kbncl spsaoe oicll ua djeol o'ncdwloi. Hdndoz nefrn neoiyn oophhlvv nmrag gstci uxwea oiihidrh asehk iehtb n'teocm hbgxg tcvso i'rfiga oagwl tsitadaued. Idita oairr h'neiu sratn iee ol tmeeh ssarn arvcl muuep scefhhou rsdmp ah.

おぉー、なんか読めそうなぐらいに英文っぽくなった。

メソッドの解説を少しだけ。

```typescript
const alphabets = 'etaoinshrdlcumwfgypbvkjxqz'
const alphabet = [...alphabets].map((char, idx) => char.repeat(alphabets.length - idx)).join('')
```
ここでは使用するアルファベットを定義しているのだが、このアルファベットの並びは英文で使用される頻度順になっている。この並び順のアルファベットを後続処理でピックされる時に頻度順で優先してピックされるように`string.repeat()`で重複させて確率を上げている。

```typescript
maxPhrases = Math.ceil(getRandomArbitrary(Math.floor(Math.abs(Math.sin(getRandomArbitrary(1, phraseLength) + 1) * maxPhrases)), maxPhrases))
```

これは指定された最大フレーズ数以下の実際に生成されるフレーズ数の上限を設定している。極端にフレーズ数が少なくならないように正弦関数を使って周期性をもたせている。

```typescript
const choicesPhraseLength = Array.from({ length: phraseLength }, (_, index) => index + 1)
const weightedValue = 5.2// Average number of letters in an English word (source by Google Books Ngram).
const weightedPhraseLength = choicesPhraseLength.map(x => Math.floor((1 / Math.abs(x - weightedValue)) * 10))
```

英単語の平均文字数である5.2を最大中央値として生成する文字数のランダム性に重みづけを持たせている。これによって引数で与えらえれたフレーズの最大文字数のうち、5文字の文字が生成される確率が上がる。

```typescript
const choicesWords = Array.from({ length: 20 }, (_, index) => index + 1)
const averageWords = 17// Average number of words in a typical English sentence (source by Google Books Ngram).
const weightedWords = choicesWords.map(x => {
  const w = Math.floor((1 / Math.abs(x - averageWords)) * 10)
  return w > 10 ? 20 : w
})
```

英文の1センテンス内に含まれる単語数の平均値が17近辺なので、17を最大中央値として生成されるセンテンスの単語数に重みづけを持たせている。これによってあまりにも短い英文や長い英文が生成されないようにしている。

- - -

本筋の開発から脱線したが、なかなか良いメソッドが作れた。
自分的には、もう lorem ipsum 使うことないかもしれん（言い過ぎ）

そのうち日本語版のダミーテキスト生成メソッドを作ってみようかな……うーん、でもマルチバイト文字の制御とか入ってくるから面倒そうだな……。