📚

「小説家になろう」のNコードの生成アルゴリズムと正規表現

2 min read

「小説家になろう」のAPIを使ったちょっとしたアプリケーションを作ろうとして、Nコードの正規表現が必要になりました。そこで軽く調査したのですが、すでにより細かい調査をしている人がいたのでそれを元にまとめてみます。

※運営元である株式会社ヒナプロジェクトに確認した内容ではないので注意してください

生成アルゴリズム

DB設計が公開されているわけではないので推測ですが、「小説家になろう」「ノクターンノベルズ」「ムーンライトノベルズ」「ミッドナイトノベルズ」で公開された小説は同一のテーブルで管理され、連番のIDが割り振られると考えられます。

この連番のIDから1を引いて[1] 9999で割った余り[2]と商を求めます。

  • 先頭→ 常に "n" です。
  • 数字部分→ 9999で割った余りに1を足し、4桁になるように0で左詰めします。
  • ローマ字部分→ 9999で割った商を "a, b, c, ..., z" を桁とするような26進法で展開します。結果が "a" になる場合を除き、余計な "a" はつけません。

たとえば連番が464784のとき、

  • 464783を9999で割った余りは4829なので、数字部分は4830。
  • 464783を9999で割った商は46。26進法では [1, 20] なので、これをローマ字に割り当てると bu

となり、Nコードは n4830bu となります。

正規表現とパース

Nコードのパースは生成時よりも自由度があり、以下の正規表現で示されるような判定になっているようです。

/^n([0-9]{4})([a-z]*)$/i

パースのルールは生成アルゴリズムの逆です。ローマ字部分を26進法で解釈して、9999をかけます。そこに数字部分を足すと完成です。

たとえば "n3957", "n0000fy", "n5040aaace", "N1337CN"[3] などは非正規なNコードです。一度連番に戻して再生成することで正規化できます。

コード例

以下にTypeScriptでのコード例を示します。ライセンスはCC0とします。

const RE_NCODE = /^n(\d{4})([a-z]*)$/i;

function parseBase26(s: string): number {
    const sl = s.toLowerCase();
    let ret = 0;
    for (let i = 0; i < sl.length; i++) {
        ret = ret * 26 + (sl.charCodeAt(i) - 0x61);
    }
    return ret;
}

function stringifyBase26(n: number): string {
    if (n === 0) return "a";
    const digits: number[] = [];
    let m = n;
    while (m > 0) {
        digits.push(m % 26 + 0x61);
        m = (m / 26) | 0;
    }
    digits.reverse();
    return String.fromCharCode(...digits);
}

export function ncodeToIndex(ncode: string): number {
    const match = RE_NCODE.exec(ncode);
    if (!match) throw new Error(`Not an ncode: ${JSON.stringify(ncode)}`);
    const lo = parseInt(match[1], 10);
    const hi = parseBase26(match[2]);
    return hi * 9999 + lo;
}

export function indexToNcode(index: number): string {
    const lo = (index - 1) % 9999 + 1;
    const hi = ((index - 1) / 9999) | 0;
    let lostr = lo.toString();
    while (lostr.length < 4) lostr = `0${lostr}`;
    return `n${lostr}${stringifyBase26(hi)}`;
}

export function normalizeNcode(ncode: string): string {
    return indexToNcode(ncodeToIndex(ncode));
}
脚注
  1. 1を引いているかどうかは観測しようがありませんが、番号が若いときに数値を一致させようとするとこの仕様になるためこの形で推測しています。 ↩︎

  2. 0000番が存在せず、指定すると直前の9999として扱われるという仕様からこのように推測しています。 ↩︎

  3. 大文字のNコードを正規形として表示している箇所もあるので、これは場合によります。 ↩︎

Discussion

ログインするとコメントできます