☀️

TypeScriptで実現する条件付きデータ操作: 日向坂46の未知なるユニットを作る

2024/08/25に公開

はじめに

この記事ではTypeScriptを使ってデータ操作や条件付けを行う方法について書きます。
具体例として、日向坂46のメンバーから特定の条件を満たすユニットを作成するプログラムを用います。

プログラムの概要

日向坂46のメンバーから4人を選抜してユニットを作るプログラムです。
ただランダムに選抜しても面白みに欠けるので、
プログラムを書いた時点では存在しないユニットを作るようにします。

設定する条件としては、まず、加入時期が異なるようにします。
※この時点で1期生・2期生・3期生・4期生が1人ずつ必ず含まれることになり、
この記事の執筆時点では存在しないユニットであることが確定します。

さらに、以下の5つについては、どれか1つの重複は許すこととします。
※どれの重複を許すかはランダムに決定します。

  • 年齢
  • 星座
  • 血液型
  • 身長
  • 出身地

また、Instagramのアカウントを持っているメンバーとそうでないメンバーがいるので、
どちらのメンバーも含まれるようにします。
(全員がアカウント未所持だったり、全員がアカウントを所持しているということは避けます。)

そんなわけで、メンバーの多様性を重視した人選となります。
ユニット名は「ダイバーシティ」とでもしておきます。

TypeScriptのデータ型の定義

日向坂46公式サイトメンバーページに記載の情報を元に定義します。

選択肢が限られるものについては、ユニオン型を用いて選択肢を最初から絞ります。

type Prefecture =
    '北海道' | '青森県' | '岩手県' | '宮城県' | '秋田県' | '山形県' | '福島県' |
    '茨城県' | '栃木県' | '群馬県' | '埼玉県' | '千葉県' | '東京都' | '神奈川県' |
    '新潟県' | '富山県' | '石川県' | '福井県' | '山梨県' | '長野県' |
    '岐阜県' | '静岡県' | '愛知県' | '三重県' |
    '滋賀県' | '京都府' | '大阪府' | '兵庫県' | '奈良県' | '和歌山県' |
    '鳥取県' | '島根県' | '岡山県' | '広島県' | '山口県' |
    '徳島県' | '香川県' | '愛媛県' | '高知県' |
    '福岡県' | '佐賀県' | '長崎県' | '熊本県' | '大分県' | '宮崎県' | '鹿児島県' | '沖縄県';

type Member = {
    id: number;
    name: string;
    birthday: Date;
    generation: 1 | 2 | 3 | 4;
    constellation: 
        '牡羊座' | '牡牛座' | '双子座' | '蟹座' | '獅子座' | 
        '乙女座' | '天秤座' | '蠍座' | '射手座' | 
        '山羊座' | '水瓶座' | '魚座';
    bloodType: 'A' | 'B' | 'O' | 'AB' | '不明';
    height: number;
    hometown: Prefecture;
    hasInstagram: boolean;
};

上記のデータ型を用い、メンバーのプロフィールを見ながらデータを作ります。
以下のような感じになります。

const hinatazaka46: Member[] = [
    { id: 1, name: "加藤史帆", birthday: new Date(1998,2,2), generation: 1, constellation: "水瓶座", bloodType: "A", 
      height: 160.5, hometown: "東京都",hasInstagram: true },
    { id: 2, name: "佐々木久美", birthday: new Date(1996,1,22), generation: 1, constellation: "水瓶座", bloodType: "O", 
      height: 168.5, hometown: "千葉県",hasInstagram: true },
    { id: 3, name: "佐々木美玲", birthday: new Date(1999,12,17), generation: 1, constellation: "射手座", bloodType: "O", 
      height: 165, hometown: "兵庫県",hasInstagram: true },
    { id: 4, name: "高瀬愛奈", birthday: new Date(1999,9,20), generation: 1, constellation: "乙女座", bloodType: "A", 
      height: 157, hometown: "大阪府",hasInstagram: false },
    { id: 5, name: "東村芽依", birthday: new Date(1998,8,23), generation: 1, constellation: "乙女座", bloodType: "O", 
      height: 154.5, hometown: "奈良県",hasInstagram: true },
    { id: 6, name: "金村美玖", birthday: new Date(2002,9,10), generation: 2, constellation: "乙女座", bloodType: "O", 
      height: 163, hometown: "埼玉県",hasInstagram: true },
    { id: 7, name: "河田陽菜", birthday: new Date(2001,7,23), generation: 2, constellation: "獅子座", bloodType: "B", 
      height: 154.4, hometown: "山口県",hasInstagram: true },
    { id: 8, name: "小坂菜緒", birthday: new Date(2002,9,7), generation: 2, constellation: "乙女座", bloodType: "O", 
      height: 161.5, hometown: "大阪府",hasInstagram: false },
    { id: 9, name: "富田鈴花", birthday: new Date(2001,1,18), generation: 2, constellation: "山羊座", bloodType: "A", 
      height: 165.2, hometown: "神奈川県",hasInstagram: true },
    { id: 10, name: "丹生明里", birthday: new Date(2001,2,15), generation: 2, constellation: "水瓶座", bloodType: "AB", 
      height: 157, hometown: "埼玉県",hasInstagram: true },
    { id: 11, name: "濱岸ひより", birthday: new Date(2002,9,28), generation: 2, constellation: "天秤座", bloodType: "A", 
      height: 167.5, hometown: "福岡県",hasInstagram: true },
    { id: 12, name: "松田好花", birthday: new Date(1999,4,27), generation: 2, constellation: "牡牛座", bloodType: "A", 
      height: 157.5, hometown: "京都府",hasInstagram: true },
    { id: 13, name: "上村ひなの", birthday: new Date(2004,4,12), generation: 3, constellation: "牡羊座", bloodType: "AB", 
      height: 162.5, hometown: "東京都",hasInstagram: true },
    { id: 14, name: "髙橋未来虹", birthday: new Date(2003,9,27), generation: 3, constellation: "天秤座", bloodType: "B", 
      height: 170, hometown: "東京都",hasInstagram: true },
    { id: 15, name: "森本茉莉", birthday: new Date(2004,2,23), generation: 3, constellation: "魚座", bloodType: "A", 
      height: 162, hometown: "東京都",hasInstagram: true },
    { id: 16, name: "山口陽世", birthday: new Date(2004,2,23), generation: 3, constellation: "魚座", bloodType: "O", 
      height: 152, hometown: "鳥取県",hasInstagram: true },
    { id: 17, name: "石塚瑶季", birthday: new Date(2004,8,6), generation: 4, constellation: "獅子座", bloodType: "A", 
      height: 153, hometown: "東京都",hasInstagram: false },
    { id: 17, name: "小西夏菜実", birthday: new Date(2004,10,3), generation: 4, constellation: "天秤座", bloodType: "B", 
      height: 164.5, hometown: "兵庫県",hasInstagram: false },
    { id: 18, name: "清水理央", birthday: new Date(2005,1,15), generation: 4, constellation: "山羊座", bloodType: "AB", 
      height: 165, hometown: "千葉県",hasInstagram: false },
    { id: 19, name: "正源司陽子", birthday: new Date(2007,2,14), generation: 4, constellation: "水瓶座", bloodType: "B", 
      height: 157.5, hometown: "兵庫県",hasInstagram: false },
    { id: 20, name: "竹内希来里", birthday: new Date(2006,2,20), generation: 4, constellation: "魚座", bloodType: "AB", 
      height: 154, hometown: "広島県",hasInstagram: false },
    { id: 21, name: "平尾帆夏", birthday: new Date(2003,7,31), generation: 4, constellation: "獅子座", bloodType: "A", 
      height: 157.5, hometown: "鳥取県",hasInstagram: false },
    { id: 22, name: "平岡海月", birthday: new Date(2002,4,9), generation: 4, constellation: "牡羊座", bloodType: "A", 
      height: 157.5, hometown: "福井県",hasInstagram: false },
    { id: 23, name: "藤嶌果歩", birthday: new Date(2006,8,7), generation: 4, constellation: "獅子座", bloodType: "不明", 
      height: 160.3, hometown: "北海道",hasInstagram: false },
    { id: 24, name: "宮地すみれ", birthday: new Date(2005,12,31), generation: 4, constellation: "山羊座", bloodType: "不明", 
      height: 164.5, hometown: "神奈川県",hasInstagram: false },
    { id: 25, name: "山下葉留花", birthday: new Date(2003,5,20), generation: 4, constellation: "牡牛座", bloodType: "O", 
      height: 160.6, hometown: "愛知県",hasInstagram: false },
    { id: 26, name: "渡辺莉奈", birthday: new Date(2009,2,7), generation: 4, constellation: "水瓶座", bloodType: "A", 
      height: 153.2, hometown: "福岡県",hasInstagram: false },
];

ユニット作成のロジック

以下を行います。

  1. 重複を許す条件をランダムに選定
  2. 日向坂46メンバーを定義した配列の並び順をシャッフルする
  3. メンバーを選定する

重複を許す条件をランダムに選定

以下のように選定します。

const conditions = ['age', 'constellation', 'bloodType', 'height', 'hometown'];
    
// 重複チェックをランダムに適用しない条件を選ぶ(ユニットのバリエーションの確保のため)
const skipCondition = conditions[Math.floor(Math.random() * conditions.length)];

まず、重複を許す条件の候補を配列として定義します。
次に、そこからランダムに選びます。

ランダムに選ぶ方法としては、ランダムに配列のインデックス番号を生成することです。
Math.random()で0以上1未満の数字がランダムに生成されます。
それに配列の長さを掛け算し、Math.floor()で結果的に小数点以下が切り捨てられることで、ランダムに配列のインデックス番号を生成できます。

日向坂46メンバーを定義した配列の並び順をシャッフルする

以下のようにシャッフルします。

const shuffleArray = (array: Member[]): Member[] => {
    for (let i = array.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [array[i], array[j]] = [array[j], array[i]];
    }
    return array;
};

// メンバーの並び順をシャッフルする(バリエーションの確保のため)
const shuffledMembers = shuffleArray(members);

shuffleArray関数では、配列の最後尾の要素から、以下のことを最前の要素まで繰り返します。

  1. 自分と同じ要素か自分より前にある要素をランダムで選ぶ
  2. ランダムで選ばれた要素と自分の位置を入れ替える(前の工程で自分が選ばれた場合は位置はそのままになる)

いわゆる、フィッシャー–イェーツのシャッフルと呼ばれるアルゴリズムです。

メンバーを選定する

これまでの処理を生かして、メンバーを選定します。
プログラムの書き方としては、以下のような感じになります。

※コメントで細かめに補足をしておきました。

const calculateAge = (birthday: Date): number => {
    const now = new Date();
    
    // 現在の年と生まれた年を引き算する
    const age = now.getFullYear() - birthday.getFullYear();

    // 今年、まだ誕生日を迎えていない場合は現在の年と生まれた年の差から1を引く必要がある。
    const monthDiff = now.getMonth() - birthday.getMonth();
    if (monthDiff < 0 || (monthDiff === 0 && now.getDate() < birthday.getDate())) {
        return age - 1;
    }
    return age;
};

const createUnit = (members: Member[]): Member[] | null => {
    const unit: Member[] = [];
    const conditions = ['age', 'constellation', 'bloodType', 'height', 'hometown'];
    
    // 重複チェックをランダムに適用しない条件を選ぶ(ユニットのバリエーションの確保のため)
    const skipCondition = conditions[Math.floor(Math.random() * conditions.length)];

    // メンバーの並び順をシャッフルする(バリエーションの確保のため)
    const shuffledMembers = shuffleArray(members);

    for (const member of shuffledMembers) {
        // 加入時期は必ずばらけさせる
        if (unit.some(m => m.generation === member.generation)) continue;

        // 重複チェックをスキップする可能性のある条件
        const age = calculateAge(member.birthday);
        if (skipCondition !== 'age' && unit.some(m => calculateAge(m.birthday) === age)) continue;
        if (skipCondition !== 'constellation' && unit.some(m =>  m.constellation === member.constellation)) continue;
        if (skipCondition !== 'bloodType' && unit.some(m => m.bloodType === member.bloodType)) continue;
        if (skipCondition !== 'height' && unit.some(m => m.height === member.height)) continue;
        if (skipCondition !== 'hometown' && unit.some(m => m.hometown === member.hometown)) continue;


        // hasInstagramのバランスをチェック
        if (member.hasInstagram && unit.filter(m => m.hasInstagram).length === 3) continue;
        if (!member.hasInstagram && unit.filter(m => !m.hasInstagram).length === 3) continue;

        unit.push(member);

        // 4人のユニットが完成したら終了
        if (unit.length === 4) {
            return unit;
        }
    }

    // 条件を満たすユニットが見つからなかった場合
    return null;
};

// ユニット作成のリトライ処理(シャッフルのされ方になどよって作成が失敗することがあるので)
const retryCreateUnit = (members: Member[], maxRetries: number = 100): Member[] | null => {
    for (let i = 0; i < maxRetries; i++) {
        const unit = createUnit(members);
        if (unit) {
            return unit;
        }
    }
    return null;
};

const unit = retryCreateUnit(hinatazaka46);

if (unit) {
    console.log("ユニット「ダイバーシティ」のメンバー:", unit.map(member => member.name));
} else {
    console.log("条件を満たすユニットが見つかりませんでした。");
}

実行結果としては、以下のような感じで、必ず1期生から4期生のそれぞれ1名ずつが選ばれた、
未知のユニットが作成されます。

もし実際に実行してみたい場合はこちらのページで「実行」を押すと実行できます。

最後に

この記事を書いた動機は、暇を持て余したおひさま(日向坂46のファンの総称)の遊びプログラミングの勉強です。

Discussion