目的
ChatGPTでデータベース設計ができるという話を聞いたので、お手並み拝見。ついでにGASで動かすところまでできないかと挑戦してみます。
結論
データベースの設計は、簡単なものであればかなり良い感じのものが出てきます。
しかし、その操作をするプログラムについては、高品質なものを出力するのはなかなか難しいと感じました(結局最後まで完成させることはできず・・・)。
ただし、実装のヒントになるレベルは全然出力できるので、ベースの部分を作ってもらうのはアリです。
また、色々なパターンのコードが出力されるので、対話を繰り返しながら案を出してもらって、その中から良さげなものをピックアップして使うのも良さそうですね。
やりとり公開
1周目
指示
有給休暇を管理するデータベースの設計案を出してください。
回答
以下は、有給休暇を管理するデータベースの簡単な設計案です。
-
テーブル: 従業員(Employee)
- 列: 従業員ID(EmployeeID), 名前(Name), メールアドレス(Email), 雇用日(HireDate)
-
テーブル: 有給休暇(PaidTimeOff)
- 列: 有給休暇ID(PaidTimeOffID), 従業員ID(EmployeeID), 休暇開始日(StartDate), 休暇終了日(EndDate), 承認者(Approver), 承認日(ApprovalDate), 状態(Status)
-
テーブル: 有給休暇の種類(PaidTimeOffType)
- 列: 有給休暇の種類ID(PaidTimeOffTypeID), 有給休暇の種類名(PaidTimeOffTypeName), 付与日数(AllocatedDays)
-
テーブル: 有給休暇履歴(PaidTimeOffHistory)
- 列: 有給休暇履歴ID(PaidTimeOffHistoryID), 有給休暇ID(PaidTimeOffID), 休暇日数(DaysTaken), 利用日(TakenDate)
上記の設計案では、従業員、有給休暇、有給休暇の種類、有給休暇履歴の4つのテーブルがあります。従業員テーブルには、従業員の基本情報が格納され、有給休暇テーブルには、従業員が取得した有給休暇の情報が格納されます。有給休暇の種類テーブルには、従業員が取得できる有給休暇の種類とその付与日数が格納されます。有給休暇履歴テーブルには、従業員が取得した有給休暇の利用履歴が格納されます。
この設計案では、従業員が有給休暇を取得するたびに、有給休暇テーブルに新しい行が追加されます。承認者によって承認されると、その行の状態が更新されます。従業員が有給休暇を利用するたびに、有給休暇履歴テーブルに新しい行が追加されます。
コメント
かなり良い雰囲気のものが出力されてきました。ただし、有効期限がなかったり、承認周りなどの不要な項目があったりするので、調整をしていきます。
2周目
指示
有給休暇には付与から2年間の有効期限があります。それを考慮して再度データベースを設計してください。
回答
以下は、有給休暇の付与から2年間の有効期限を考慮したデータベースの設計案です。
-
テーブル: 従業員(Employee)
- 列: 従業員ID(EmployeeID), 名前(Name), メールアドレス(Email), 雇用日(HireDate)
-
テーブル: 有給休暇(PaidTimeOff)
- 列: 有給休暇ID(PaidTimeOffID), 従業員ID(EmployeeID), 休暇開始日(StartDate), 休暇終了日(EndDate), 承認者(Approver), 承認日(ApprovalDate), 状態(Status), 有効期限(ExpirationDate)
-
テーブル: 有給休暇の種類(PaidTimeOffType)
- 列: 有給休暇の種類ID(PaidTimeOffTypeID), 有給休暇の種類名(PaidTimeOffTypeName), 付与日数(AllocatedDays)
-
テーブル: 有給休暇履歴(PaidTimeOffHistory)
- 列: 有給休暇履歴ID(PaidTimeOffHistoryID), 有給休暇ID(PaidTimeOffID), 休暇日数(DaysTaken), 利用日(TakenDate)
上記の設計案では、有給休暇テーブルに有効期限(ExpirationDate)の列を追加して、有給休暇の付与日から2年後の日付を格納します。これにより、有給休暇の有効期限が過ぎたものは、有給休暇テーブルから自動的に削除することができます。
有給休暇履歴テーブルには、従業員が取得した有給休暇の利用履歴が格納されます。このテーブルは、有給休暇テーブルとは独立しており、有給休暇の有効期限が過ぎても、有給休暇履歴は削除されません。
コメント
指示通りに修正してくれました。かなり良い感じです。RDB(リレーショナルデータベース)の設計はAIとめちゃくちゃ相性良さそうな雰囲気あります。
3周目
指示
承認者、承認日のカラムは不要です。その上で、有給休暇を付与するとき、および消化するときは、どのようなデータベースの操作を行えば良いですか?
回答
承認者と承認日のカラムが不要となった場合、以下のようなデータベース操作を行うことで、有給休暇の付与や消化を行えます。
-
有給休暇の付与:
- 従業員ID(EmployeeID)、有給休暇の種類ID(PaidTimeOffTypeID)、付与日数(AllocatedDays)を指定して、有給休暇テーブル(PaidTimeOff)に新しいレコードを追加します。
- 有効期限(ExpirationDate)は、付与日から2年後の日付を自動的に計算して格納するようにします。
-
有給休暇の消化:
- 従業員ID(EmployeeID)、休暇開始日(StartDate)、休暇終了日(EndDate)を指定して、有給休暇テーブル(PaidTimeOff)から該当するレコードを検索します。
- 有給休暇が有効であることを確認し、有給休暇履歴テーブル(PaidTimeOffHistory)に新しいレコードを追加します。
- 有給休暇の消化に伴い、有給休暇テーブルの休暇日数(DaysTaken)を更新します。
- 有給休暇の残日数は、有給休暇テーブルの付与日数(AllocatedDays)から休暇日数(DaysTaken)を引いたものとなります。
以上のように、データベースには、有給休暇テーブル、有給休暇の種類テーブル、有給休暇履歴テーブルを設定し、これらを操作することで、有給休暇の付与や消化を管理することができます。
コメント
かなり精度の高い回答です。といっても細かいところまでは記載がないので、掘り下げていきましょう。
4周目
指示
上記データベースのER図を書いてください。
回答
申し訳ありませんが、私(ChatGPT)は画像を含む回答を提供することができません。ただし、有給休暇管理のER図を作成する場合は、以下のようなテーブルを含めることができます。
-
従業員テーブル(Employee)
- 従業員ID(EmployeeID) - 主キー
- 名前(FirstName)
- 姓(LastName)
- その他の属性(雇用日など)
-
有給休暇テーブル(PaidTimeOff)
- 休暇ID(PaidTimeOffID) - 主キー
- 従業員ID(EmployeeID) - 外部キー
- 有給休暇の種類ID(PaidTimeOffTypeID) - 外部キー
- 付与日(AllocatedDate)
- 付与日数(AllocatedDays)
- 消化日数(DaysTaken)
- 有効期限(ExpirationDate)
-
有給休暇種別テーブル(PaidTimeOffType)
- 有給休暇の種類ID(PaidTimeOffTypeID) - 主キー
- 有給休暇の種類名(PaidTimeOffTypeName)
-
有給休暇履歴テーブル(PaidTimeOffHistory)
- 履歴ID(HistoryID) - 主キー
- 休暇ID(PaidTimeOffID) - 外部キー
- 従業員ID(EmployeeID) - 外部キー
- 有給休暇の種類ID(PaidTimeOffTypeID) - 外部キー
- 休暇開始日(StartDate)
- 休暇終了日(EndDate)
このようなER図を作成することで、従業員と有給休暇の関係、有給休暇と有給休暇の種類の関係、有給休暇と有給休暇履歴の関係を表現することができます。
コメント
ER図は作成できないみたいですが、かなりそれに近い情報が出てきました。有能すぎる。
5周目
指示
有給休暇履歴テーブルには、休暇開始日と休暇終了日のカラムがありますが、この管理方法だと、その休暇開始日と休暇終了日の間の日が休日かどうかを確認する必要があります。そのため、1レコード1日として管理する方法も考えられますが、どちらが良いですか?
回答
休暇履歴テーブルには、休暇開始日と休暇終了日のカラムがある場合、確かに休日かどうかを確認する必要があります。例えば、週末や祝日が休暇期間に含まれる場合、有給休暇の消化日数が正確に計算できなくなる可能性があります。
一方、1レコード1日として管理する方法は、膨大な数のレコードが必要になるため、テーブルのサイズが大幅に拡大することが予想されます。これは、データベースのパフォーマンスに悪影響を与える可能性があります。
結局のところ、どちらの方法が良いかは、システムの使用目的や必要な情報によって異なります。有給休暇の消化日数が正確に必要であれば、1日1レコードとして管理する方法が望ましいかもしれません。一方、パフォーマンスが重要であれば、休暇開始日と終了日の間に含まれる日数を確認する方法が望ましいかもしれません。
コメント
お手本のような回答ですね。こういう教科書的な問いにはやはり強いです。
6周目
指示
では有給休暇履歴テーブルは1レコード1日で管理する方法を採用し、このデータベースをGoogleスプレッドシートで実装します。その上で、従業員ID、休暇取得日の2つの情報を入力すると、対象の従業員に付与されている有給休暇のうち、有効期限が短いものから消化されるシステムをGoogle Apps Scriptで作成したいです。このソースコードを作成してください。また、あわせて従業員ID、休暇取得日の2つの情報を入力するUIも作成してください。
回答
以下は、Google Apps Scriptで有給休暇を消化するシステムと、UIを作成するサンプルコードです。従業員ID、休暇取得日の2つの情報を入力すると、有効期限が短い有給休暇から順に消化されます。
function doGet() {
var template = HtmlService.createTemplateFromFile('Page');
return template.evaluate();
}
function consumeVacationDays(employeeId, vacationDate) {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('VacationHistory');
var range = sheet.getDataRange();
var values = range.getValues();
var today = new Date();
var vacationDays = [];
// 従業員IDと休暇取得日が一致する有給休暇履歴を取得する
for (var i = 1; i < values.length; i++) {
var record = values[i];
if (record[0] == employeeId && record[1].toString() == vacationDate) {
vacationDays.push(record);
}
}
// 有効期限が短い順にソートする
vacationDays.sort(function(a, b) {
return new Date(a[3]) - new Date(b[3]);
});
// 有効期限が切れるまで有給休暇を消化する
for (var i = 0; i < vacationDays.length; i++) {
var record = vacationDays[i];
var expirationDate = new Date(record[3]);
expirationDate.setFullYear(expirationDate.getFullYear() + 2);
if (expirationDate >= today) {
sheet.deleteRow(i + 2);
break;
}
}
}
function processForm(form) {
var employeeId = form.employeeId;
var vacationDate = form.vacationDate;
consumeVacationDays(employeeId, vacationDate);
}
また、UIを作成するために、HTMLとCSSを以下のように作成します。このファイルは「Page.html」という名前で保存します。
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<style>
body {
font-family: Arial, sans-serif;
}
input[type="text"] {
padding: 10px;
margin: 5px 0;
border: 1px solid #ccc;
border-radius: 4px;
}
input[type="submit"] {
background-color: #4CAF50;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
</head>
<body>
<h2>有給休暇消化システム</h2>
<form id="myForm">
<label for="employeeId">従業員ID</label><br>
<input type="text" id="employeeId" name="employeeId"><br>
<label for="vacationDate">休暇取得日</label><br>
<input type="text" id="vacationDate" name="vacationDate"><br>
<input type="submit" value="消化
コメント
こちらの回答、実は1回Regenerate(再生成)しました。コードが途中で止まってしまっったので。しかし2回目も、コードの生成が途中で止まってしまいました。長いのは厳しいのでしょうか。ちなみに個人的には1回目のコードの方が好きでした。
7周目
指示
変数の宣言には可能な限りconstを使用し、コード内で変更する必要があるものはletとしてください。varはどうしても必要なとき以外は使用しないでください。その上で、コードを書く直してください。
回答
以下に修正版のコードを示します。
function usePaidLeave() {
// UIから従業員IDと休暇取得日を取得
const employeeId = Number(document.getElementById('employee-id-input').value);
const vacationDate = new Date(document.getElementById('vacation-date-input').value);
// 従業員IDから有効な有給休暇情報を取得
const validPaidLeaves = getValidPaidLeaves(employeeId);
// 有効な有給休暇情報のうち、有効期限が短いものから消化する
validPaidLeaves.sort((a, b) => a.expirationDate - b.expirationDate);
for (const paidLeave of validPaidLeaves) {
const remainingDays = getRemainingDays(paidLeave, vacationDate);
if (remainingDays > 0) {
// 有効期限が短い有給休暇から消化する
consumePaidLeave(paidLeave, remainingDays);
break;
}
}
}
// 従業員IDに対応する有効な有給休暇情報を取得する
function getValidPaidLeaves(employeeId) {
const validPaidLeaves = [];
const today = new Date();
for (const paidLeave of PAID_LEAVES) {
if (paidLeave.employeeId === employeeId && paidLeave.expirationDate >= today) {
validPaidLeaves.push(paidLeave);
}
}
return validPaidLeaves;
}
// 有給休暇の残り日数を計算する
function getRemainingDays(paidLeave, vacationDate) {
const vacationDays = Math.ceil((vacationDate - paidLeave.startDate) / (24 * 60 * 60 * 1000));
return Math.min(vacationDays, paidLeave.remainingDays);
}
// 有給休暇を消化する
function consumePaidLeave(paidLeave, consumedDays) {
paidLeave.remainingDays -= consumedDays;
updatePaidLeave(paidLeave);
}
// 有給休暇情報を更新する
function updatePaidLeave(paidLeave) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName('有給休暇');
const row = sheet.getDataRange().getValues().findIndex(row => row[0] === paidLeave.employeeId && row[1] === paidLeave.startDate);
sheet.getRange(row + 1, 3).setValue(paidLeave.remainingDays);
}
修正点は以下の通りです。
- 変数の宣言にはconstを使用し、コード内で変更する必要があるものはletとしています。
- varは使用していません。
- for...in文ではなくfor...of文を使用しています。
- PAID_LEAVES配列の要素を直接変更するのではなく、updatePaidLeave関数を使用しています。
コメント
そんな丸ごと書き変えろとは言ってませんよ・・・???
また、それっぽいけど、よく見ると色々おかしい感じのコードです。履歴情報どこいった。
8周目
指示
sheet.getRange関数の引数内に直接1や3といった数値を記載せず、各シートの情報(テーブル)はgetDataRange関数で取得し、カラムの指定にはヘッダ名を使用してください。なお各シートにはA1セルからデータが入力されており、1行目がヘッダ、2行目からがデータレコードであることとします。
回答
以下が修正後のコードになります。
function usePaidLeave() {
// UIから従業員IDと休暇取得日を取得
const ui = SpreadsheetApp.getUi();
const response = ui.prompt('従業員IDと休暇取得日を入力してください(例:A001,2023/03/10)', ui.ButtonSet.OK_CANCEL);
if (response.getSelectedButton() !== ui.Button.OK) {
// キャンセルされた場合は処理を中断
return;
}
// 入力された値を変数に代入
const inputValues = response.getResponseText().split(',');
const employeeId = inputValues[0];
const leaveDate = new Date(inputValues[1]);
// 有給休暇履歴シートから該当従業員のレコードを取得
const leaveSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('有給休暇履歴');
const leaveDataRange = leaveSheet.getDataRange();
const leaveValues = leaveDataRange.getValues();
const employeeLeaveRecords = leaveValues.filter(row => row[0] === employeeId);
// 有効期限の短い順にソート
employeeLeaveRecords.sort((a, b) => new Date(a[2]) - new Date(b[2]));
// 休暇を消化するレコードを更新
let consumedLeaveDays = 0;
for (let i = 0; i < employeeLeaveRecords.length; i++) {
const leaveRecord = employeeLeaveRecords[i];
const leaveStartDate = new Date(leaveRecord[1]);
const leaveEndDate = new Date(leaveRecord[2]);
if (leaveStartDate <= leaveDate && leaveEndDate >= leaveDate) {
// 消化する休暇に該当するレコードを更新
const daysToConsume = Math.ceil((leaveEndDate.getTime() - leaveDate.getTime()) / (1000 * 60 * 60 * 24));
leaveSheet.getRange(leaveRecord[4] + 1, leaveRecord[5] + 2).setValue(leaveRecord[6] - daysToConsume);
consumedLeaveDays += daysToConsume;
break;
} else {
// 消化する休暇に該当しないレコードはスキップ
consumedLeaveDays += (leaveEndDate - leaveStartDate) / (1000 * 60 * 60 * 24) + 1;
}
}
// 結果を表示
const remainingLeaveDays = employeeLeaveRecords[0][6] - consumedLeaveDays;
ui.alert(`${leaveDate.getFullYear()}年${leaveDate.getMonth() + 1}月${leaveDate.getDate()}日に${consumedLeaveDays}日の有給休暇を消化しました。残りの有給休暇は${remainingLeaveDays}日です。`);
}
コメント
あんまり指示が通ってないのと、やっぱりほぼ書き直されてる・・・
コードレベルの微修正を投げるのは難しいかもしれません。
9周目
指示
このコードだと有給休暇履歴テーブルのレコードが削除されないでしょうか?有給休暇履歴テーブルのレコードは削除しないでください。その代わり、有給休暇テーブルで対応するレコードの消化日数を加算するようにしてください。
回答
申し訳ありません、先程の回答に誤りがありました。有給休暇履歴テーブルのレコードが削除される問題がありますので、以下のように修正したコードを提供いたします。
function usePaidLeave(employeeId, leaveDate) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const employeesSheet = ss.getSheetByName('従業員');
const paidLeaveSheet = ss.getSheetByName('有給休暇');
const paidLeaveHistorySheet = ss.getSheetByName('有給休暇履歴');
// 従業員の行を取得
const employeeRow = getRowByValue(employeesSheet, 1, employeeId);
if (employeeRow === -1) {
throw new Error(`従業員ID ${employeeId} が見つかりませんでした。`);
}
// 従業員の有給休暇付与日数を取得
const paidLeaveCount = employeesSheet.getRange(employeeRow, 3).getValue();
// 有効期限内の有給休暇を取得し、消化日数が少ない順にソート
const paidLeaveData = paidLeaveSheet.getDataRange().getValues().slice(1);
const validPaidLeaveData = paidLeaveData.filter(row => row[0] === employeeId && row[2] >= new Date()).sort((a, b) => a[2] - b[2]);
// 消化日数を加算して、有給休暇テーブルを更新する
let remainingDays = paidLeaveCount;
for (let i = 0; i < validPaidLeaveData.length && remainingDays > 0; i++) {
const row = validPaidLeaveData[i];
const days = row[3];
if (remainingDays >= days) {
row[3] = 0;
remainingDays -= days;
} else {
row[3] -= remainingDays;
remainingDays = 0;
}
paidLeaveSheet.getRange(i + 2, 4).setValue(row[3]);
}
// 有給休暇履歴テーブルに消化履歴を追加する
paidLeaveHistorySheet.appendRow([employeeId, leaveDate, remainingDays]);
// 従業員テーブルの有給休暇付与日数を更新する
employeesSheet.getRange(employeeRow, 3).setValue(remainingDays);
}
function getRowByValue(sheet, column, value) {
const dataRange = sheet.getDataRange();
const data = dataRange.getValues();
for (let i = 1; i < data.length; i++) {
if (data[i][column - 1] === value) {
return i + 1;
}
}
return -1;
}
修正点は以下の通りです。
- 有給休暇履歴テーブルにはレコードを追加するだけで、削除はしないように修正しました。
- 有給休暇
コメント
最後まで喋れず途中で切れてしまいまいした。また、コードがJSではなく、なぜかscssとして出力されました(流石に読みにくいので、↑ではJSとしています)。
しかし、処理自体はなぜか突然良い感じになりました。
10周目
指示
良いコードですね。現在各シートのデータを二次元配列として扱っていますが、これをオブジェクトとして扱うようにコードを書き換えることはできますか?
回答
はい、可能です。二次元配列からオブジェクトに変換するには、まず各列のヘッダーを取得し、各行の値をそのヘッダーに対応するプロパティに格納する必要があります。
以下は、オブジェクトとして扱うためのコード例です。
function getVacationData(employeeId, vacationDate) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const vacationSheet = ss.getSheetByName('有給休暇');
const historySheet = ss.getSheetByName('有給休暇履歴');
const vacationDataRange = vacationSheet.getDataRange();
const vacationData = vacationDataRange.getValues();
const vacationHeaders = vacationData[0];
const historyDataRange = historySheet.getDataRange();
const historyData = historyDataRange.getValues();
const historyHeaders = historyData[0];
const vacationIndex = vacationHeaders.indexOf('従業員ID');
const expirationIndex = vacationHeaders.indexOf('有効期限');
const remainingIndex = vacationHeaders.indexOf('残り日数');
const historyEmployeeIndex = historyHeaders.indexOf('従業員ID');
const historyStartIndex = historyHeaders.indexOf('休暇開始日');
const historyEndIndex = historyHeaders.indexOf('休暇終了日');
const historyUsedIndex = historyHeaders.indexOf('消化日数');
const vacationObj = {};
vacationData.slice(1).forEach(row => {
vacationObj[row[vacationIndex]] = {
remaining: row[remainingIndex],
expiration: row[expirationIndex]
};
});
let usedVacationDays = 0;
historyData.slice(1).forEach(row => {
if (row[historyEmployeeIndex] === employeeId && new Date(row[historyStartIndex]).getTime() <= new Date(vacationDate).getTime() && new Date(row[historyEndIndex]).getTime() >= new Date(vacationDate).getTime()) {
usedVacationDays += row[historyUsedIndex];
}
});
const resultObj = {
remaining: vacationObj[employeeId].remaining - usedVacationDays,
expiration: vacationObj[employeeId].expiration
};
return resultObj;
}
このコードでは、vacationObjオブジェクトを使って従業員ごとの残り日数と有効期限を管理し、forEachメソッドを使ってオブジェクトを作成しています。また、resultObjオブジェクトを使って返り値を管理し、最後にそのオブジェクトを返します。
コメント
やたら冗長なのが出てきました。あと、データ取得以外のことが吹っ飛んでしまってますね。
11周目
指示
もう少しシンプルに書けませんか?
回答
はい、以下のように書き換えることができます。
function useVacation() {
const sheetNames = ["従業員", "有給休暇"];
const data = {};
sheetNames.forEach(name => {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(name);
const values = sheet.getDataRange().getValues();
const headers = values[0];
const records = values.slice(1);
data[name] = records.map(record => {
const obj = {};
headers.forEach((header, i) => {
obj[header] = record[i];
});
return obj;
});
});
const { employee, vacation } = data;
const id = "0001";
const date = new Date("2023/03/04");
const validVacations = vacation.filter(vacation => vacation.employeeId === id && vacation.expirationDate >= date);
validVacations.sort((a, b) => a.expirationDate - b.expirationDate);
const usedDays = 1;
if (validVacations.length > 0) {
const targetVacation = validVacations[0];
const targetIndex = vacation.findIndex(vacation => vacation.id === targetVacation.id);
vacation[targetIndex].usedDays += usedDays;
const row = vacation[targetIndex].row;
const column = vacation[targetIndex].column;
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("有給休暇履歴");
sheet.getRange(row, column).setValue(vacation[targetIndex].usedDays);
}
}
このコードでは、dataオブジェクトに各シートの情報をオブジェクトとして格納し、以降は配列のように操作することができます。また、各レコードのデータをオブジェクトとして扱うことで、obj[header]のようにヘッダ名を直接キーとして使用することができます。
コメント
またscssで出てきました。コードのパーツとしての利用可能性はありそうな感じですが、当初の目的とは離れてしまっています。この辺りで切り上げます!