2022年12月7日水曜日

年間祝日勤務回数の平準化

 月内での平準化は、GUI上で出来ますが、年間で見た平準化は、出来ないのでExcelを使います。

Excelを使うとなると、色々なアプローチが考えられますが、スケジュールナースでは、以下のような手順でExcelを使います。

1)プロジェクトをExcelにエクスポート

2)1)ファイルのうち、必要な部分だけ(スタッフ定義)を残して別ファイルとします

3)2)シートに年間集計というシートを追加します

4)3)シートで累計処理を行い、スタッフ定義シートに反映させます

5)3)スタッフ定義シートをスケジュールナースにインポートします

という流れになります。(後でGithubにサンプルプロジェクトをアップしておきます。)

1)プロジェクトを表ーExcel出力でExportします

行制約を除く殆どの表はExcelに出力されます。シート名が表の名前になります
必要なシートのみ残して後は削除し名前をつけて保存します。
今回必要なシートは、スタッフ定義のみです。
スタッフ定義では、その月の祝日勤務回数を制約として与える部分と、
その月までの累計の偏差(X-最小値)を要素として持つようにします。
Excelシートをスタッフ定義に読み込みます。
Excel集計部でワーク用です。このシートは、スケジュールナース表名とは一致しないので
読みこまれることはありません。このシートで今月の制約と累計を維持しています。
10ヶ月経過したときのプロパティの様子です。偏差は、右端になりますが、1以下になっています。(これは、上出来で、まれな例です)
このときの集計シートです。

その月までの累計偏差が分かれば、マニュアルでその月の制約を設定することが出来ます。累計偏差は、スタッフ定義シートに書き込まれていますから、それを読み込めばよい訳です。

やっていることは、それほど難しくはないので、Excel表作成出来る方ならば、出来ると思います。

以下は、Pythonでこれを自動化しようという話で上級になります。

さらに進んで、この部分をPythonで記述しました。以下がそのソースになります。Pythonでは、Pywin32で直接にExcel本体を動かしています。なので、Excel本体がないと動きません。

スケジュールナースが通常行っているExcelのインポート・エクスポートは、3rdパーティ(Syncfusion社)による実装であり、Excelの有無に関係なく動作します。Excel本体を動かすのに比べ、遥かに高速ですが、ExcelのFormula、特にロケールに関連する演算に難があります。(openpyxlの同じような問題を内包していると言えます。)

その点、Pywin32が動かしているのはExcel本体なので、上記のような問題はありません。しかし、動作が遅いので、勤務表そのものを出力しようとすると、規模によりますが、ときに耐え難いと感じることがあります。今回、データ量は多くないので時間は問題になりません。

では、Pythonのアルゴリズム解説です。

def post_main()が、求解動作が終わった後に起動されるメイン部になります。

下記で言語制約の使用、と Pythonポスト処理 両方にチェックが入っているときに

起動されます。右ペインに求解後にポスト処理に行っている様子が分かります。




求解動作は、毎月何度も行われます。それに対してスタッフ定義の制約が設定されるのは、Excel読み込み時だけです。

<年間集計シートの初期化>

年間集計シートがないときに生成するようにしています。累計初期値は、0です。

<スタッフ定義シートの更新>

制約開始日を保存するようにしておいて、今月制約開始日と一致しないときに、累計処理

を行います。

一致の判定は、テキストで行っています。”2022-12-05”で保存するとExcel内では、日付形式となってしまい、見た目のテキストが変わってしまいます。そのため、NumberFormatLocalで、形式を上記形式で指定しています。これによりテキスト比較が可能になります。


累計は偏差辞書リストにソートして、スタッフ定義シートにコピーします。

print(my_list)で、右ペインに下のように表示されています。

[(7.0, [1, 3, 4, 5, 6, 8, 10, 13, 14]), (6.0, [0, 2, 7, 9, 11, 12, 15, 16, 17])]

これは、Keyの値(累計回数)とその値を持つpersonのリストです。辞書をソートすると、上のようにタプルのリストになるのですね。

daydef、今月祝..等不明な変数は、ソース全体で検索するとその定義が判明します。

#pdb.set_trace()のコメントアウトを外すとデバッグが起動します。

この動作は、通常翌月へ、移行時の最初の一回目の求解になります。Excelシートが更新されているので、インポートします。それ以降が、実際の求める制約に基づく求解となります。

<祝勤務カウント>

「今月祝」の日に、解が公休でないときをカウントしています。シフト解を参照しています。(タスクプロジェクトの場合は、タスク解・シフト解の両方をPythonから呼び出すことが可能です。)

<制約アルゴリズム>

偏差が3以内のとき →制約しない(祝回数を最大値に設定)

else :

    祝累計仕事回数が最も多い →制約Max0

    次に多い →制約Max1

    ....

という具合にしています。割りにいい加減なアルゴリズムですが、最終的にはスタッフ定義上で如何様にでも、横の累計偏差を眺めながらユーザが再設定し直すことができるので、良しとしました。

Pywin32のドキュメントは殆ど見当たらないのですが、大抵は、こちらを参照して事足りています。

<まとめ>

特にExcel読み込み以外に指示は要りません。Excel本体は必要ですが、スケジュールナースだけで処理が完結できます。Excelは、一瞬表示されますが、操作は必要ありません。

<評価>

リソースに余裕がある職場ではそれなりに動くと思います。リソースに余裕のない職場で

これを行うことはお勧めしません。平準化を行うという行為自体が探索空間を狭めるために、この強度が高いと、他に悪影響を及ぼす可能性が高いと思われます。ソフト制約化は、必須であり、弱めの強度とすることをお勧めします。


import sys
import os
import datetime

def make_holiday_working_count(list):
    for person in 全スタッフ:
        cnt=0
        for day in 今月祝:
            shift=shift_solution[person][day]
            if shift!="公休":
                cnt+=1
        list[person]=cnt

def write_holiday_cnt(ws,list):
    c=ws.Range("B3")
    pn=0
    for person in 全スタッフ:
        ws.Cells(c.Row+pn,c.Column).Value=list[person]
        pn+=1

def write_cumulative_value(ws,list):
    ws.Range("C2").Value=ws.Range("B2").Value#制約開始日を移動
    ws.Range("B2").Value=daydef[制約開始日]#今月の制約開始日を設定
    c=ws.Range("B3")
    
    for person in 全スタッフ:
        ws.Cells(c.Row+person,c.Column+1).Value +=ws.Cells(c.Row+person,c.Column).Value #累計処理
        list[person]=ws.Cells(c.Row+person,c.Column+1).Value
        

def set_staff_property(wb,list,offset_list):
    ws = wb.Worksheets("スタッフ定義")
    ws.Activate()
    d=ws.Range("B3").Value
    c=ws.Range("A1:Z2").Find("祝日勤務回数属性")
    d=ws.Range("A1:Z2").Find("祝日勤務回数累計偏差属性")

    if c==None:
        print("can not find 祝日勤務回数属性")
        exit()
    if d==None:
        print("can not find 祝日勤務回数累計偏差属性")
        exit()
    row_offset=2
    #print(list)
    #print(c.Row)
    for person in 全スタッフ:
        ws.Cells(c.Row+row_offset+person,c.Column).Value =list[person]
        ws.Cells(d.Row+row_offset+person,d.Column).Value =offset_list[person]

def calc_and_set_staff_property(wb,list):
    my_dic = {}#Dictionary key:祝日勤務した回数 value=list[person] 
    pn=0
    for value in list:
        my_dic.setdefault(value, []).append(pn)
        pn+=1
    my_list=sorted(my_dic.items(),reverse=True)#Reverseソート 2list value-tubpleになる
    最大累計回数=my_list[0][0] 
    最小累計回数=my_list[-1][0]
    
    print(最大累計回数,最小累計回数)
    #offset list作成
    holiday_works_offset_list=[0]*len(全スタッフ)
    for mykey in my_list:
            for value in mykey[1]:#tuple
                holiday_works_offset_list[value]=int(mykey[0]-最小累計回数)
    print(holiday_works_offset_list)

    #max list作成
    holidays_length=len(今月祝)
    holiday_works_max_list=[0]*len(全スタッフ)
    
    print(my_list)
    if (最大累計回数-最小累計回数)>=3:#差が3より小さいならholidays_lengthを全スタッフにセットして終了
        max_cnt=0
        persons_set_cnt=0
        for mykey in my_list:
            for value in mykey[1]:#tuple
                holiday_works_max_list[value]=max_cnt
                persons_set_cnt +=1
            if persons_set_cnt>=3:#3人ずつ許容を増やす
                max_cnt +=1
                persons_set_cnt=0
    else:
        for person in 全スタッフ:
            holiday_works_max_list[person]=holidays_length
    print(holiday_works_max_list)
    set_staff_property(wb,holiday_works_max_list,holiday_works_offset_list)#Exelへ書き込み

def make_boarder(ws,address):
    xlContinuous=1
    xlMedium=-4138
    xlCenter=-4108
    ws.Range(address).Borders.Color = int("".join(list(reversed(["000000"[i: i+2] for i in range(0, 6, 2)]))),16)
    ws.Range(address).Borders.LineStyle = xlContinuous
    ws.Range(address).Borders.Weight = xlMedium
    ws.Range(address).Font.Bold = True
    ws.Range(address).HorizontalAlignment = xlCenter

def make_initial_format(ws):
    

    ws.Range("B1").Value="今月"
    ws.Range("B1").EntireColumn.AutoFit()#列幅自動
    make_boarder(ws,"B1")

    ws.Range("C1").Value="先月までの累計"
    ws.Range("C1").EntireColumn.AutoFit()#列幅自動
    make_boarder(ws,"C1")

    ws.Range("A2").Value="制約開始日"
    ws.Range("A2").EntireColumn.AutoFit()#列幅自動
    make_boarder(ws,"A2")

    ws.Range("B2").Value=daydef[制約開始日]
    make_boarder(ws,"B2")
    ws.Range("B2").NumberFormatLocal="yyyy-mm-dd;@" #2022-12-06 format

    ws.Range("C2").Value=daydef[制約開始日]
    make_boarder(ws,"C2")
    ws.Range("C2").NumberFormatLocal="yyyy-mm-dd;@" #2022-12-06 format

    c=ws.Range("A3")
    pn=0
    for person in 全スタッフ:
        ws.Cells(c.Row+pn,c.Column).Value=staffdef[person]
        ws.Cells(c.Row+pn,c.Column+1).Value=0 #初期0
        ws.Cells(c.Row+pn,c.Column+2).Value=0 #初期0
        pn+=1

def post_main():
    
    import pdb
    #pdb.set_trace()
    print('\n\n*********ポスト処理を実行中です。*************\n')
    import win32com.client#pywin32をインポート

#すでにExcelが起動されている場合はそのタスクが使われる
#エラー終了するとタスクは残ります
    try :
        xl = win32com.client.Dispatch("Excel.Application")
    except:
        print("can not invoke excel")
        exit()
    #動いている様子を見てみる
    xl.Visible = True

    os.chdir(project_file_path)
    file=os.path.join(project_file_path,"年間祝日回数設定.xlsx")
    file.replace("/","\\") #
    wb = xl.Workbooks.Open(file)


    print(wb.Worksheets.Count)
    for i in range(0, wb.Worksheets.Count):
        print(wb.Worksheets[i].name)

    try:
        ws = wb.Worksheets("年間集計")
        ws.Activate()
    except:
        print("年間集計シートがないので作成します")
        wb.Worksheets.Add()
        wb.Worksheets(1).Name = "年間集計"
        ws = wb.Worksheets("年間集計")
        ws.Activate()
        make_initial_format(ws)
    dtext=ws.Range("B2").Text
    #print(ws.Range("B2").NumberFormatLocal)
    if dtext!= daydef[制約開始日]:
        print("今月制約日が違うので累計処理を実行します")
        cumlative_value_list=[0]*len(全スタッフ)#list0初期化
        write_cumulative_value(ws,cumlative_value_list)
        print("祝日回数のスタッフプロパティを設定します")
        calc_and_set_staff_property(wb,cumlative_value_list)

    holiday_work_cnt_list=[0]*len(全スタッフ)#list0初期化
    make_holiday_working_count(holiday_work_cnt_list)#解から祝 公休でないをカウント
    write_holiday_cnt(ws,holiday_work_cnt_list)#今月結果を書き込み

    # Excelシートオブジェクト
    #ws = wb.Worksheets(1)

    # 指定したシートを選択
    # Select()の使用前にシートのActivate()が必要
    #ws.Activate()
    wb.Close(True)# Trueで保存。False=Defaultでブックを保存せずにクローズ
    # Excel終了
    xl.Quit()
    print('\n\n*********ポスト処理を実行終了しました。*************\n')

2022年12月5日月曜日

Redo/Undoのグレーアウト化

 「Redo/Undoでラベルはグレーアウトの方がよい

これにより「ここにあったような気がするんだけど 見当たらないですね」というユーザ同士の行き違いを減らすことができると思いました。」


というご指摘があり実装しました。

現在のところ、適用はストア版のみです。(未だpublicになってはいません)


2022年12月4日日曜日

職権訂正

 特許料年金の払い込みでミスがあり、特許庁より電話がありました。

軽微なミスだったので、職権訂正で事なきを得ました。特許庁から電話を頂くのは、通算二度目です。お手間を取らせてごめんなさい。


2022年12月3日土曜日

ソフト制約調整

動画をアップしました。

ソフト制約システムは、結局トレードオフです。しかも、事前にどうなるか、誰も予測不能です。やってみる(Cut&Try)しかありません。(これが為に、高速ソルバーが必要となります。)

 https://youtu.be/Hb9UyidMG-g

管理者が、ある程度周りの状況を掴んだ上で、狙い目を設定することになるでしょう。

2022年12月2日金曜日

ストア版永続ライセンスのURL配布

下記に該当する方々に、URLの配布を開始しました。 

■現ライセンスを保持している

■開発に貢献があった

■Covid-19関連

■βテスター業務を完了

手作業で1ユーザ毎、順次配信しているので、該当しているが未だ来ないという方は、しばしお待ちください。

E-mailでURLをお送りしています。

インストール手順です。

前提条件:

■microsoft アカウントがあること

■Windows10/11 64bit Versionであること

手順:

受領したURLリンクをクリックするとマイクロソフトアカウントが求められます。

(下記は仮想環境(英語)の為、英語表示になっていますが日本語で出ます)

マイクロソフトアカウントのパスワードを入れます
するとマイクロソフトアカウントに入ります。25桁のコードが既に入っている筈です。
次へをクリックします。
確認をクリックします。
入手をクリックします。
サイトを開きます。
永続ライセンス版(50000円)の購入ページが開きます。
50000円をクリックします。25桁のコードが正しければ課金されることはありませんので、ご安心ください。(万が一、間違っていた場合でも、課金される場合は、金額が表示され確認のダイアログで出ます。その場合は最初からやり直してください)
ダウンロードが始まります。開くが出るまで10-20分位かかることもあります。
サーバエラー等によりダウンロードが失敗していると思われる場合、下記のライブラリを開くと再試行が表示されている場合があります。その場合は、再試行してください。






日本語スケジュールナースが開きます。(下記は英語環境なので英語表記になっています)


あとは、通常通りアプリの起動でOKです。スケジュールナースのUpdateは自動で行われるのでダウンロードする必要はありません。

ストア版永続ライセンスは、マイクロソフトアカウントに紐づいています。なので、

同じマイクロソフトアカウントでログインすれば、他のPCでも10台までお使いになれます。PCを買い換えても、使うことが出来ます。(永続版ストアサイトは、privateですがhttps://www.microsoft.com/store/apps/9NGKL40LLGGT



2022年12月1日木曜日

ストア版は自動更新される

 今日、ストア版とdownload版、両方のupdateを行いました。

で、「この更新を必須にします」にチェックをつけてリリースしました。


数時間後に、何気に起動してみると、しっかり更新されていました。一度インストールすると、Windowsが勝手にパッケージUpdateを察知してインストールしてくれるようです。この間、Windowsのサインアウトはしていません。パッケージ容量は現在80MB程度もあり、結構時間がかかるので、いちいちダウンロードしなくてもよくなるのはうれしいです。(ありがた迷惑になる場面もあるかもしれませんが。)

=>

■12月1日リリースしたものが12月5日

■12月5日リリースしたものが12月5日

になって自動更新されていました。

リリース後、数時間から数日のバラツキがある模様です。