サイゼリヤのメニューを組み合わせるサイトを作ってみた

# はじめに
予算を入力するとサイゼリヤのメニューをランダムで出力してくれるサイトを制作してみました。
今回は、機能のみの制作で、cssを使わずに作成しました。
メニューはcsvファイルにまとめておいて、それを読み込む形式としました。

```:環境
windows10
vscode
```
ルール
- メニュー番号・メニュー名・値段を出力
- 消費税は10%として計算
- 同じメニューは１回だけ選択
- 予算の80%以上を使うように設定
- メニューは2023年の関東のもの
- お子様メニューやアルコールは除く
- ドリンクバーは個別に追加するかしないかを決定

# 1.html
非常にシンプルとなっております
```html:menu.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>サイゼリヤメニュー組み合わせ</title>
</head>
<body>
    <h1>サイゼリヤメニュー組み合わせツール</h1>
    <ul>
        <li>メニュー番号・メニュー名・値段が出力されます</li>
        <li>消費税は10%として計算しています</li>
        <li>同じメニューは１回だけ選択されます</li>
        <li>予算の80%以上を使うように設定しています</li>
        <li>メニューは2023年の関東のものを使用しています</li>
        <li>お子様メニューやアルコールは除いてます</li>
    </ul>
    <input type="number" id="price-input" placeholder="予算を入力（円）" />
    <label>
        <input type="checkbox" id="drink-bar-checkbox"> ドリンクバーを含める
    </label>
    <button id="combine-button">組み合わせを表示</button>
    <div id="menu-combination"></div>
    <script src="menu.js"></script>
</body>
</html>

```
# 2.JavaScript

csvを読み込むメソッドです。
asyncとawaitを使って、csvファイルが完全に読み込めてから組み合わせを行えるようにします。
これがないと、メニューの組み合わせを行うことが出来ませんでした。
```javascript:csvを読み込む
let menuData = [];

// CSVを非同期に読み込む関数
async function loadCSV() {
    const response = await fetch('menu.csv');
    const text = await response.text();
    const rows = text.split('\n').slice(1); // ヘッダーを除外
    rows.forEach(row => {
        const [menu_number, menu_name, price] = row.split(',');
        if (menu_number && menu_name && price) {
            menuData.push({ menu_number, menu_name, price: Number(price) });
        }
    });
}
```
チェックボックスでドリンクバー(DB01)を含める場合は最優先で計算に含めるようにした
基本的には、予算を超えない範囲でランダムで追加していく作業
（本当は、カロリーとかもいれてナップザック法とか最適化問題のアルゴリズムを入れ込みたかったけど、時間優先で作った）
```javascript:メニューを組み合わせる関数
async function combineMenu(budget) {
    await loadCSV(); // CSVファイルの読み込みを待つ
    const includeDrinkBar = document.getElementById('drink-bar-checkbox').checked;
    let combinedMenus = [];
    let totalPrice = 0;
    let potentialMenus = menuData.slice(); // メニューデータのコピーを作成
    let budgetThreshold = budget * 0.8; // 予算の80%
// ドリンクバーを含める場合、先に追加する
    if (includeDrinkBar) {
        const drinkBar = potentialMenus.find(menu => menu.menu_number === 'DB01');
        if (drinkBar && budget >= drinkBar.price) {
            combinedMenus.push(drinkBar);
            totalPrice += drinkBar.price;
        }
    }
    while (totalPrice < budgetThreshold && potentialMenus.length > 0) {
        let randomIndex = Math.floor(Math.random() * potentialMenus.length);
        let menu = potentialMenus[randomIndex];

        if (totalPrice + menu.price <= budget) {
            combinedMenus.push(menu);
            totalPrice += menu.price;
        }
        potentialMenus.splice(randomIndex, 1); // 選んだメニューはリストから削除
    }

    // 予算の80%以上を使い切れなかった場合、ランダムにメニューを追加して埋める
    if (totalPrice < budgetThreshold && combinedMenus.length > 0) {
        // まだ選ばれていないメニューから追加で選ぶ
        while (totalPrice < budgetThreshold) {
            let remainingMenus = menuData.filter(menu => 
                !combinedMenus.includes(menu) && totalPrice + menu.price <= budget);
            if (remainingMenus.length > 0) {
                let randomMenu = remainingMenus[Math.floor(Math.random() * remainingMenus.length)];
                combinedMenus.push(randomMenu);
                totalPrice += randomMenu.price;
            } else {
                break; // 追加できるメニューがない場合は終了
            }
        }
    }

    //合計金額が予算を超えていないかチェック
    if (totalPrice > budget) {
        // 予算を超える場合は、最後に追加したメニューを取り除く
        let lastMenu = combinedMenus.pop();
        totalPrice -= lastMenu.price;
    }

    return { combinedMenus, totalPrice };
}
```
税抜きと税込み価格を表示させるようにした
```javascript:表示関数
function displayCombination(combination) {
    const container = document.getElementById('menu-combination');
    const taxRate = 1.1; // 消費税
    const totalPriceWithTax = Math.ceil(combination.totalPrice * taxRate); // 税込み価格（切り上げ）
    container.innerHTML = `<p>合計金額（税抜き）: ${combination.totalPrice}円</p><ul>`;
    container.innerHTML += `<p>税込み価格: ${totalPriceWithTax}円</p><ul>`;
    
    combination.combinedMenus.forEach(menu => {
        container.innerHTML += `<li>${menu.menu_number}: ${menu.menu_name} - ${menu.price}円</li>`;
    });

    container.innerHTML += '</ul>';
}
```
出来上がったJSファイル
```javascript:menu.js
let menuData = [];

// CSVを非同期に読み込む関数
async function loadCSV() {
    const response = await fetch('menu.csv');
    const text = await response.text();
    const rows = text.split('\n').slice(1); // ヘッダーを除外
    rows.forEach(row => {
        const [menu_number, menu_name, price] = row.split(',');
        if (menu_number && menu_name && price) {
            menuData.push({ menu_number, menu_name, price: Number(price) });
        }
    });
}
// メニューを組み合わせる関数
async function combineMenu(budget) {
    await loadCSV(); // CSVファイルの読み込みを待つ
    const includeDrinkBar = document.getElementById('drink-bar-checkbox').checked;
    let combinedMenus = [];
    let totalPrice = 0;
    let potentialMenus = menuData.slice(); // メニューデータのコピーを作成
    let budgetThreshold = budget * 0.8; // 予算の80%
// ドリンクバーを含める場合、先に追加する
    if (includeDrinkBar) {
        const drinkBar = potentialMenus.find(menu => menu.menu_number === 'DB01');
        if (drinkBar && budget >= drinkBar.price) {
            combinedMenus.push(drinkBar);
            totalPrice += drinkBar.price;
        }
    }
    while (totalPrice < budgetThreshold && potentialMenus.length > 0) {
        let randomIndex = Math.floor(Math.random() * potentialMenus.length);
        let menu = potentialMenus[randomIndex];

        if (totalPrice + menu.price <= budget) {
            combinedMenus.push(menu);
            totalPrice += menu.price;
        }
        potentialMenus.splice(randomIndex, 1); // 選んだメニューはリストから削除
    }

    // 予算の80%以上を使い切れなかった場合、ランダムにメニューを追加して埋める
    if (totalPrice < budgetThreshold && combinedMenus.length > 0) {
        // まだ選ばれていないメニューから追加で選ぶ
        while (totalPrice < budgetThreshold) {
            let remainingMenus = menuData.filter(menu => 
                !combinedMenus.includes(menu) && totalPrice + menu.price <= budget);
            if (remainingMenus.length > 0) {
                let randomMenu = remainingMenus[Math.floor(Math.random() * remainingMenus.length)];
                combinedMenus.push(randomMenu);
                totalPrice += randomMenu.price;
            } else {
                break; // 追加できるメニューがない場合は終了
            }
        }
    }

    //合計金額が予算を超えていないかチェック
    if (totalPrice > budget) {
        // 予算を超える場合は、最後に追加したメニューを取り除く
        let lastMenu = combinedMenus.pop();
        totalPrice -= lastMenu.price;
    }

    return { combinedMenus, totalPrice };
}

// 組み合わせ結果を表示する関数
function displayCombination(combination) {
    const container = document.getElementById('menu-combination');
    const taxRate = 1.1; // 消費税
    const totalPriceWithTax = Math.ceil(combination.totalPrice * taxRate); // 税込み価格（切り上げ）
    container.innerHTML = `<p>合計金額（税抜き）: ${combination.totalPrice}円</p><ul>`;
    container.innerHTML += `<p>税込み価格: ${totalPriceWithTax}円</p><ul>`;
    
    combination.combinedMenus.forEach(menu => {
        container.innerHTML += `<li>${menu.menu_number}: ${menu.menu_name} - ${menu.price}円</li>`;
    });

    container.innerHTML += '</ul>';
}


// 組み合わせボタンのイベントハンドラ
document.getElementById('combine-button').addEventListener('click', async () => {
    const budget = Number(document.getElementById('price-input').value);
    const combination = await combineMenu(budget); // 非同期処理を待つ
    displayCombination(combination);
});

```
# 3.出来上がったサイト
しばらくサイゼリヤに行かない間に、注文は紙にメニュー番号を書き込んで行う形式になってたよね～
![SnapCrab_NoName_2023-11-20_22-25-38_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531555/a81b7f97-5315-cd9a-8b3b-1af10cb0e02f.png)

# 4.まとめ
- デザインやUI面を考えたい
- アプリでも作れたらいいなぁ
- アニメーションとかあってもいいかも
- カロリーも考慮して最適化問題のアルゴリズムを組んでみたい
- 画像とかを読み込んで自動的にCSVを作りたい
- サーバーがないので公開ができない。。。