総合病院で処方され、保険薬局で調剤
図1.総合病院で処方され、保険薬局で調剤された薬剤のQRコード |
剤は1剤のみであるが、注意事項があるのでQRコードは3つに分かれている。まず、1つ目のQRコード(左端)には以下の内容が格納されている。
JAHISTC04,1 1,佐藤 一郎,1,19461203,,,,,,,サトウ イチロウ 5,20210301,1 11,長谷川薬局・大山店,23,4,0113541,695-0231,和歌山県周南市北区坂田6-20-15,056-321-0225,1 51,総合病院 和歌山緑十字病院,23,1,9812365,1 55,芥川 茶一郎,循環器内科,1 201,1,ビソプロロールフマル酸塩
リスト1.1
次に、2番目のQRコード(中央)には次のような内容が格納されている。
錠2.5mg「日医工」,1,錠,4,2123016F1131,1 281,1,1回1錠,1 291,1,自己判断でお薬をやめると症状が悪化することがあります。めまい、ふらつきがおこることがあります。車の運転や機械を操作する時には注意して下さい。,1 291,1,妊婦は通常服用できません。息切れ、むくみ、脈がと
リスト1.2
最後に、3番目のQRコード(右端)には次のような内容が格納されている。
ぶ、徐脈などの症状が出た場合はご連絡下さい。,1 301,1,1日1回 朝食後,43,日分,1,1,,1
リスト1.3
1つのQRコードに格納できる容量は決まっているため、上記の例のように行の途中であってもぶった切られる。したがって、QRコードの読み込みに際しては、1番目から(左端から)順番に一つずつ読み込んで格納されているデータをマージしていかないといけない。
3つのQRコードに格納されていたデータをマージしたものを下記に示す。
JAHISTC04,1 1,佐藤 一郎,1,19461203,,,,,,,サトウ イチロウ 5,20210301,1 11,長谷川薬局・大山店,23,4,0113541,695-0231,和歌山県周南市北区坂田6-20-15,056-321-0225,1 51,総合病院 和歌山緑十字病院,23,1,9812365,1 55,芥川 茶一郎,循環器内科,1 201,1,ビソプロロールフマル酸塩錠2.5mg「日医工」,1,錠,4,2123016F1131,1 281,1,1回1錠,1 291,1,自己判断でお薬をやめると症状が悪化することがあります。めまい、ふらつきがおこることがあります。車の運転や機械を操作する時には注意して下さい。,1 291,1,妊婦は通常服用できません。息切れ、むくみ、脈がとぶ、徐脈などの症状が出た場合はご連絡下さい。,1 301,1,1日1回 朝食後,43,日分,1,1,,1
リスト1.4
このフォーマットは、JAHISの電子版お薬手帳フォーマットに準じており、仕様書がネット上からダウンロードできる(現在の最新バージョンはVer.2.4)。
リスト1.4はコンマ区切りのCSV形式になっており、その役割が先頭フィールドのレコードNoで決まっている(下表参照)。
表1.レコードNo |
リスト1.4の1行目はバージョン情報である。これは、"JAHISTC"という文字列で始まり、それに続く2桁の数字でバージョンを表す(この例ではバージョン4)。
2行目のレコードNoは1で、これは患者情報レコードで、次のフィールドを持っている。
表2.患者情報レコードの各フィールド |
表3.調剤等年月日レコードの各フィールド |
表4.調剤-医療機関等レコードの各フィールド |
表5.処方-医療機関レコードの各フィールド |
表6.処方-医師レコードの各フィールド |
表7.薬品レコードの各フィールド |
表8.薬品補足レコードの各フィールド |
表9.薬品服用注意レコードの各フィールド |
表10.用法レコード |
なお、リスト1.4の例では処方番号(RP番号)は1つしかなかったが、一般的に、複数のRPがある場合はレコードNoの201~301が下図のように繰り返される。
図2.RP番号 |
処方データパーサ
リスト1.4のようなJAHISフォーマットの処方データを入力として与えて処方オブジェクトに変換するプログラムを以下に示す。
/** * 処方データ(JAHISフォーマット)を処方オブジェクトに変換する */ function barcode2obj(lines) { var prescription = { patientInfo: { No1: {}, No2: [] }, dispensing: [], }; var dispensing = { institution: { No5: {}, No11: {}, No15: {}, No51: {}, No55: {}, }, RPs: [], }; var RP = { drugs: [], dosage: {}, }; lines.forEach(function(line){ var items = line.split(','); switch(items[0]) { // 患者基本情報(レコードNo.1 患者情報レコード) case "1": prescription.patientInfo.No1 = { name: items[1], gender: items[2], birthdate: items[3], zipcode: items[4], address: items[5], phone: items[6], emergencyContact: items[7], bloodType: items[8], weight: items[9], kana: items[10], }; break; // 基本情報(レコードNo.2 患者特記レコード) case "2": prescription.patientInfo.No2.push({ type: items[1], content: items[2], creator: items[3], }); break; // 調剤-医療機関情報(レコードNo.5 調剤等年月日レコード) case "5": dispensing.institution.No5 = { date: items[1], creator: items[2], }; break; // 調剤-医療機関情報(レコードNo.11 調剤-医療機関等レコード) case "11": dispensing.institution.No11 = { name: items[1], prefCode: items[2], tensuCode: items[3], code: items[4], zipCode: items[5], address: items[6], phone: items[7], creator: items[8], }; break; // 調剤-医師・薬剤師情報(レコードNo.15 調剤-医師・薬剤師レコード) case "15": dispensing.institution.No15 = { name: items[1], contact: items[2], creator: items[3], }; break; // 処方-医療機関情報(レコードNo.51 処方-医療機関レコード) case "51": dispensing.institution.No51 = { name: items[1], prefCode: items[2], table: items[3], code: items[4], creator: items[5], }; break; // 処方-医師情報(レコードNo.55 処方-医師レコード) case "55": dispensing.institution.No55 = { name: items[1], department: items[2], creator: items[3], }; break; // 薬品情報(レコードNo.201 薬品レコード) case "201": RP.drugs.push({ No201: { rp: items[1], name: items[2], dose: items[3], unit: items[4], codeType: items[5], code: items[6], creator: items[7], } }); break; // 薬品補足情報(レコードNo.281 薬品補足レコード) case "281": RP.drugs[RP.drugs.length - 1].No281 = { rp: items[1], supplement: items[2], creator: items[3], }; break; // 薬品服用注意情報(レコードNo.291 薬品服用注意レコード) case "291": RP.drugs[RP.drugs.length - 1].No291 = { rp: items[1], attention: items[2], creator: items[3], }; break; // 用法情報(レコードNo.301 用法レコード) case "301": RP.dosage = { No301: { rp: items[1], name: items[2], quantity: items[3], unit: items[4], formCode: items[5], dosageType: items[6], dosageCode: items[7], creator: items[8], } }; dispensing.RPs.push(RP); RP = { drugs: [], dosage: {}, }; break; default: console.log('default:' + items[0]); break; } }); prescription.dispensing.push(dispensing); return prescription; }
リスト2.1 処方パーサ
この関数は、リスト1.4の各行を要素とする文字列配列を引数に取り、以下のような処方オブジェクトを返す。
{ "patientInfo": { "No1": { "name": "佐藤 一郎", "gender": "1", "birthdate": "19461203", "zipcode": "", "address": "", "phone": "", "emergencyContact": "", "bloodType": "", "weight": "", "kana": "サトウ イチロウ" }, "No2": [] }, "dispensing": [ { "institution": { "No5": { "date": "20210301", "creator": "1" }, "No11": { "name": "長谷川薬局・大山店", "prefCode": "23", "tensuCode": "4", "code": "0113541", "zipCode": "695-0231", "address": "和歌山県周南市北区坂田6-20-15", "phone": "056-321-0225", "creator": "1" }, "No15": {}, "No51": { "name": "総合病院 和歌山緑十字病院", "prefCode": "23", "table": "1", "code": "9812365", "creator": "1" }, "No55": { "name": "芥川 茶一郎", "department": "循環器内科", "creator": "1" } }, "RPs": [ { "drugs": [ { "No201": { "rp": "1", "name": "ビソプロロールフマル酸塩錠2.5mg「日医工」", "dose": "1", "unit": "錠", "codeType": "4", "code": "2123016F1131", "creator": "1" }, "No281": { "rp": "1", "supplement": "1回1錠", "creator": "1" }, "No291": { "rp": "1", "attention": "妊婦は通常服用できません。息切れ、むくみ、脈がとぶ、徐脈などの症状が出た場合はご連絡下さい。", "creator": "1" } } ], "dosage": { "No301": { "rp": "1", "name": "1日1回 朝食後", "quantity": "43", "unit": "日分", "formCode": "1", "dosageType": "1", "dosageCode": "", "creator": "1" } } } ] } ] }
リスト2.2 処方オブジェクト
上記オブジェクトがprescriptionという変数に格納されているとすると、処方データはprescription.dispensing[0].RPsという配列でアクセスできる。
このプログラムの問題点は調剤情報が複数ある場合を想定していない点である。レコード出力順は下図のようになっている(処方-医師レコードを出力する場合)。
図3.レコード出力順 |
QRコード読み取り
下図は実際にQRコードを読み取る流れを示したものである。
図4.QRコード読み取りの流れ |
この流れを状態遷移図で表したものが下図である。
図5.QRコード読み取り処理の状態遷移図 |
図6.ユーザ管理シート |
図5の状態遷移図に従って実装したユーザメッセージ処理関数であるrespondUser()を以下に示す。
function respondUser(userMessage,userId) { let res = new Response(); const userSheet = getUserSheet(userId); // statusに応じて処理を変える if (userSheet.status == 1) { input_QR_data(res, userSheet, userMessage); } else { // ユーザメッセージに応じてレスポンスを変える if(userMessage == '・・・'){ ・ ・ (省略) ・ } else if(userMessage.match(/^JAHISTC*/)){ input_QR_data(res, userSheet, userMessage); } } return res.getMessage(); } /** * QRデータ読み込み処理 */ function input_QR_data(res, userSheet, userMessage) { if (userSheet.status == 0) { //QRコード読み取り(初回) userSheet.sheet.getRange(3, 1).setValue(new Date()); userSheet.sheet.getRange(3, 2).setValue(userMessage); userSheet.sheet.getRange(2, 2).setValue(1); } else if(!userMessage.match(/^\^_\^*/)){ //QRコード読み取り(途中) const buf = userSheet.sheet.getRange(3, 2).getValue(); userSheet.sheet.getRange(3, 2).setValue(buf + userMessage); } else { //QRコード読み取り(終了) userSheet.sheet.getRange(2, 2).setValue(0); const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('処方データ'); const buf = userSheet.sheet.getRange(3, 2).getValue(); const prescription = barcode2obj(buf.trim().split('\n')); userSheet.sheet.getRange(3, 3).setValue(JSON.stringify(prescription,null,' ')); // 読み込んだ処方情報をカレンダーに登録 const location = prescription.dispensing[0].institution.No11.name; for(let RP of prescription.dispensing[0].RPs) { createEventsFromRP(RP,location); } sheet.appendRow([new Date(), buf.trim()]); res.pushLineText('QRコードの読み込みを終了しました!'); return; } res.pushLineText('次のQRコードを読み込んでください。もう読み込むべきQRコードがなければ^_^を入力してください。'); }
リスト3.1 respondUser関数
ユーザが入力したテキストメッセージを捌くresponseUser()関数は、ステータス(userSheet.status)の値に応じて処理を分岐し、状態遷移図(図5)に示すように、ステータスが1の場合はQRコード入力状態になっており、関数input_QR_data()を呼び出している。そうでない場合(ステータスが0の場合)は文字列が「JAHISTC」で始まる場合は最初のQRコードの読み取りなので関数input_QR_data()を呼び出している。
関数input_QR_data()では、ステータスが0の場合は初回QRコード読み取り処理なので、状態遷移図(図5)にあるように、ユーザシートのセルA3に処理日時を記録し、読み取ったQRコードの内容をセルB3へ設定し、ステータス(セルB2)を"1"に設定する。
ステータスが0でない場合、メッセージが"^_^"でなければQRコードの読み取りの続きなので、読み取った内容をユーザシートのセルB3へ追記する。メッセージが"^_^"であれば、QRコード読み取りは終了したので、ステータス(セルB2)を"0"に設定し、読み取った処方データ(セルB3)をシート「処方データ」へ転記後、処方パーサbarcode2obj()関数を使って処方データオブジェクトに変換し、createEvent()関数を利用してGoogleカレンダーに服薬予定を設定する。
createEventsFromRP()関数とそれに付随する諸関数を以下に示す。
/** * 処方データからカレンダーに服用スケジュールを登録 */ function createEventsFromRP(RP, location){ // 用法名称 const dose = getDosageTable(RP.dosage.No301.name); // 服薬タイミング const timing = ["朝", "昼", "夜", "寝"]; // 食前か食後か? const before_after = dose["食"]; // 服薬タイミング(take_timing)ごとに服用の有無(doesTake)と食前か食後の区分を表示する for(let i = 0; i < timing.length; i++) { const take_timing = timing[i]; //take_timing = [朝|昼|夜|寝] //doesTake = 薬があるか('1')ないか('0') //before_after=食前('前')か食後('後')か var doesTake = dose[take_timing]; if(doesTake == '1'){ for(let j = 0; j < RP.dosage.No301.quantity; j++) { var title = (take_timing == "寝" ? "就寝前" : (take_timing + "の食" + before_after)) + "の薬(未)"; var start_time = getStartTime(take_timing, before_after, j); var end_time = getEndTime(start_time); var description = getDescription(RP); createEvent(title,description,location,start_time,end_time); } } } } /** * イベントを作成する関数 */ function createEvent(title,description,location,start_time,end_time){ //既に同タイトル、同開始~終了時刻のイベントが作成されていたら説明に追記 const events = CalendarApp.getDefaultCalendar().getEvents(start_time,end_time, {search: title}); if(events.length > 0) { const event = events[0]; Logger.log('イベント既存:' + event.getTitle() + ',' + event.getDescription()); event.setDescription(event.getDescription() + '\n' + description); return; } //イベントの新規作成 var event = CalendarApp.getDefaultCalendar().createEvent( title, start_time, end_time, { description:description, location:location } ); event.setColor(CalendarApp.EventColor.RED); } /** * descriptionに入れる薬品名の編集 */ function getDescription(RP){ const descriptions = []; for(let drug of RP.drugs){ descriptions.push(drug.No201.name + ' ' + drug.No201.dose + drug.No201.unit); if(drug.No281) { descriptions.push(drug.No281.supplement); } if(drug.No291) { descriptions.push(drug.No291.attention); } } return descriptions.join('\n'); } /** * timingと食前・後を引数にして開始日時を返す関数 * @param {String} take_timing [朝|昼|夜|寝] * @param {String} before_after [前|後] * @param {Integer} offset [日数のオフセット] * @return {String} 開始日時 */ function getStartTime(take_timing, before_after, offset){ let h = 0; let m = 0; // timingのデフォルト時刻を求めるために設定取得 var settings = getSettings(); if(take_timing == "朝"){ h = settings.morning.hours; m = settings.morning.minutes; } else if (take_timing == "昼"){ h = settings.lunch.hours; m = settings.lunch.minutes; } else if (take_timing == "夜"){ h = settings.dinner.hours; m = settings.dinner.minutes; } else if (take_timing == "寝"){ h = settings.sleep.hours; m = settings.sleep.minutes; } const dt = Utilities.formatDate(new Date(),'JST','yyyy.MM.dd.HH.mm').split('.'); const str_date = Utilities.formatString('%04d-%02d-%02dT%02d:%02d+09:00',dt[0], dt[1], dt[2], h, m); const start_time = new Date(str_date); const minutes = before_after == "前" ? 0 : 30; start_time.setUTCMinutes(start_time.getUTCMinutes() + minutes); start_time.setUTCDate(start_time.getUTCDate() + offset); return start_time; } /** * 終了時刻を求める関数 * 【処理】開始時刻を引数にして、その1時間後を終了時刻とする */ function getEndTime(start_time){ return new Date( start_time.getFullYear(), start_time.getMonth(), start_time.getDate(), start_time.getHours() + 1, start_time.getMinutes(), start_time.getSeconds() ); } /** * 設定ローディング * 設定ファイル:「服薬管理デモ」 * @return {Object} settings 設定情報 */ function getSettings() { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('設定'); if (sheet == null) { //設定シートがない場合 const settings = { morning: {hours: 7, minutes: 00}, lunch: {hours: 12, minutes: 00}, dinner: {hours: 18, minutes: 00}, sleep: {hours: 23, minutes: 00}, }; return settings; } const data = sheet.getRange('B2:C5').getValues(); const settings = { morning: {'hours': data[0][0], 'minutes': data[0][1]}, lunch: {'hours': data[1][0], 'minutes': data[1][1]}, dinner: {'hours': data[2][0], 'minutes': data[2][1]}, sleep: {'hours': data[3][0], 'minutes': data[3][1]}, }; Logger.log(JSON.stringify(settings, null, ' ')); return settings; }
リスト3.2 カレンダーに服薬スケジュールを登録
createEventsFromRP()関数の第一引数RPはリスト2.2の処方オブジェクトの配列RPs(正確にはdispensing[0].RPs)の要素で、第二引数locationはレコードNo11に格納されている調剤薬局の名前(処方オブジェクトのdispensing[0].institution.No11.name)である。
この関数ではまずgetDosageTable関数を呼び出して用法から服用タイミング等の情報を取得する。 次いで、2重のfor文で朝・昼・夕・就寝の4つの服用タイミング毎に投与日数分だけcreateEvent関数を使ってGoogleカレンダーに服用予定イベントを作成する。各服用タイミングでの服用の有無は配列dose[]でチェックする。また、服用開始時刻は関数getStartTimeによって求める。この関数は引数にタイミング(朝・昼・夕・寝)と食前・食後の区分(before_afte)、offsetを取り、事前に設定された時刻(これはスプレッドシート「設定」に事前に設定されている値を関数getSettingsによって読みとっている)と引数の情報から計算している(食後は30分ずらす。offsetは一定時間ずらす場合に指定するが、通常は0である)。 服用終了時刻は関数getEndTimeで計算する。この関数は引数で指定された開始時刻の1時間後を返すようになっている。
Googleカレンダーにイベントを作成する関数はcreateEvent関数である。 この関数は引数にタイトル(title)、説明(description)、場所(location)、開始時刻(start_time)、終了時刻(end_time)をとり、タイトルには服用タイミングと食前・食後の区分から、例えば「朝の食後の薬(未)」といった文字列を設定する。ここで末尾の「(未)」は未服用の「未」で、服用したら「済」に別プロセスにて更新される。説明(description)には関数getDescriptionを使って処方データから薬品名、数量、単位などから編集した薬の情報が設定される。下図にcreateEvent関数で作成されたGoogleカレンダーのイベントを示す。
図8.Googleカレンダーに設定されたイベント |
なお、利用するGoogleカレンダーはリスト3.2にあるようにCalendarApp.getDefaultCalendar()で取得しており、Googleアカウントのデフォルトカレンダーになっている。
getDosageTable関数は以下のように辞書テーブル(仮想配列)DOSAGE_TABLE_から対応する用法の服用タイミング等の情報を返している。なお、用法名称がDOSAGE_TABLE_に存在しない場合は関数parseDossageを使って用法名称を解析して服用タイミングを可能な限り推定しようとしている。しかしながら推定には限界があるので、事前にどのような用法名称があるか調べてDOSAGE_TABLE_に設定しておくのが望ましい。
/**----------------------------------------------------------------- * * 【用法操作関数群】 * 用法から服用タイミング等の情報を取得する * DOSAGE_TABLE_にある用法はそこから、ない場合は正規表現を使って可能な限り情報を抽出する * *----------------------------------------------------------------*/ function getDosageTable(dosage) { if (DOSAGE_TABLE_[dosage]) { return DOSAGE_TABLE_[dosage]; } else { return parseDossage(dosage); } } /** * 正規表現を使って可能な限り用法から情報を抽出する */ function parseDossage(dosage) { const flag = { "朝": "0", "昼": "0", "夜": "0", "食": "", "寝": "0" }; const result = /^【?1日(.)回 ?(毎|朝|昼|夕|朝夕)食(前|後)(及び就寝前)?に?】?$/.exec(dosage); Logger.log(dosage + ' -> ' + result); if(!result){ return flag; } const timing = result[2] == '毎' ? '朝昼夕' : result[2]; for(let i = 0; i < timing.length; i++) { let t = timing.charAt(i); t = t == '夕' ? '夜' : t; flag[t] = '1'; } flag['食'] = result[3]; flag['寝'] = result[4] ? '1' : '0'; return flag; } const DOSAGE_TABLE_ = { "【1日4回毎食後及び就寝前に】": { "朝": "1", "昼": "1", "夜": "1", "食": "後", "寝": "1" }, "【1日3回毎食前に】": { "朝": "1", "昼": "1", "夜": "1", "食": "前", "寝": "0" }, "【1日3回毎食後に】": { "朝": "1", "昼": "1", "夜": "1", "食": "後", "寝": "0" }, "毎食後服用": { "朝": "1", "昼": "1", "夜": "1", "食": "後", "寝": "0" }, "【1日2回朝夕食前に】": { "朝": "1", "昼": "0", "夜": "1", "食": "前", "寝": "0" }, "【1日2回朝食後及び就寝前に】": { "朝": "1", "昼": "0", "夜": "0", "食": "後", "寝": "1" }, "【1日2回朝夕食後に】": { "朝": "1", "昼": "0", "夜": "1", "食": "後", "寝": "0" }, "【1日1回朝食後に】": { "朝": "1", "昼": "0", "夜": "0", "食": "後", "寝": "0" }, "【1日1回昼食後に】": { "朝": "0", "昼": "1", "夜": "0", "食": "後", "寝": "0" }, "【1日1回夕食後に】": { "朝": "0", "昼": "0", "夜": "1", "食": "後", "寝": "0" }, "【1日1回朝食前に】": { "朝": "1", "昼": "0", "夜": "0", "食": "前", "寝": "0" }, "【1日1回昼食前に】": { "朝": "0", "昼": "1", "夜": "0", "食": "前", "寝": "0" }, "【1日1回夕食前に】": { "朝": "0", "昼": "0", "夜": "1", "食": "前", "寝": "0" }, "【1日1回就寝前に】": { "朝": "0", "昼": "0", "夜": "0", "食": "前", "寝": "1" }, "1日1回 朝食後": { "朝": "1", "昼": "0", "夜": "0", "食": "後", "寝": "0" }, }
リスト3.3 用法から服用タイミングを取得
設定シート
服用タイミングから服用開始時刻を計算する際に、あらかじめタイミングごとに時刻をスプレッドシート(シート名は「設定」)に設定しておくと、それがデフォルトとして利用される(下図)。
図9.「設定」シート |
もし、このシートがなければ関数getSettingsはリスト3.2にあるようなデフォルト値を返すことになる。設定シートに書き込むには関数setSettingsを利用する。
/** * 設定書き込み * 設定ファイル:「服薬管理デモ」 * @param {Object} settings 設定情報 */ function setSettings(settings) { Logger.log(JSON.stringify(settings, null, ' ')); var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('設定'); if (sheet == null) { // シートがない場合は作成する sheet = SpreadsheetApp.getActiveSpreadsheet().insertSheet(); sheet.setName('設定'); } sheet.getRange("A1:C5").setValues([ ['項目','時','分'], ['朝食',settings.morning.hours,settings.morning.minutes], ['昼食',settings.lunch.hours,settings.lunch.minutes], ['夕食',settings.dinner.hours,settings.dinner.minutes], ['就寝',settings.sleep.hours,settings.sleep.minutes] ]); }
リスト4.1 設定書き込み
この関数は、引数に指定された次のフォーマットの服用開始時刻をシート「設定」に書き込む。もし、シートがなければ作成する。
var settings = { morning: {hours: 7, minutes: 30}, lunch: {hours: 12, minutes: 30}, dinner: {hours: 18, minutes: 30}, sleep: {hours: 23, minutes: 30}, };
リスト4.2 服用開始時刻
0 件のコメント:
コメントを投稿