2022年6月30日木曜日

インスタンス22世界記録更新

 Timothy Curtoisさんからお祝いメールを頂きました。 

新しい結果、おめでとうございます。このインスタンスを最適性が証明されるように解いたことは、とても素晴らしい成果です。他の人もきっと興味を持つでしょう。

ありがとうございます。Timothy Curtoisさんは、ナーススケジューリング分野での重鎮です。恐らく、この分野において最も多くの論文を執筆されており、このベンチマークの設計者でもあります。

Timothy CURTOIS | Managing Director | BSc, PhD (researchgate.net)


2022年6月28日火曜日

Instance22 Exact Solution (Updated World Record)

 I've found the new updated and exact solution on Instance 22 in scheduling benchmarks. The solution is the best one in the search Space of 10^18953, means 1 googol ^189.53, with proven that no better solution exists.


It took almost a day using AMD 5950X(16 cores 32threads), schedule nurse3 155A.

I wonder quantum annealing machines can achieve this performance?

2022年6月27日月曜日

this.handleを追加

 次のVersionでGUIのメインフォームのハンドルthis.handle(HWND=xx)を追加します。

なので、EnumWindowsでハンドル探しをする必要がなく、次のようにダイレクトに記述できます。(HWNDは、EnumWindowsや、SPY++したハンドルとは異なりますが、問題なく動くようです。)




import win32clipboard
import win32gui
import ctypes

#---------------------------
xlHairline = 1
xlThin     = 2
xlThick    = 4
xlMedium   = -4138

    # ------------------------------------------------------------------
    # Excel Enum XlFileFormat
    # ------------------------------------------------------------------
xlCSV                         =  6
xlHtml                        = 44
xlWorkbookDefault             = 51
xlOpenXMLWorkbook             = 51
xlOpenXMLWorkbookMacroEnabled = 52
xlWorkbookNormal              = -4143
xlCurrentPlatformText         = -4158

    # ------------------------------------------------------------------
    # Excel Enum XlLineStyle
    # ------------------------------------------------------------------
xlContinuous    =  1
xlDashDot       =  4
xlDashDotDot    =  5
xlSlantDashDot  = 13
xlDash          = -4115
xldot           = -4118
xlDouble        = -4119
xlLineStyleNone = -4142
    # ------------------------------------------------------------------
    # Excel Enum XlOrientation
    # ------------------------------------------------------------------
xlHorizontal = -4128
xlVertical   = -4166
xlDownward   = -4170
xlUpward     = -4171
 
# ------------------------------------------------------------------
#https://docs.microsoft.com/en-us/office/vba/api/excel.xlformatconditionoperator
xlCellValue=1
xlExpression=2
xlBetween=1#	Between. Can be used only if two formulas are provided.
xlEqual=3#	Equal.
xlGreater=5#	Greater than.
xlGreaterEqual=7#	Greater than or equal to.
xlLess=6#	Less than.
xlLessEqual=8#	Less than or equal to.
xlNotBetween=2#	Not between. Can be used only if two formulas are provided.
xlNotEqual=4#	Not equal.
xlUnderlineStyleSingle           = 2 
xlCenter      = -4108

MB_OK=0x00000000#The message box contains one push button: OK. This is the default.
MB_OKCANCEL=0x00000001#The message box contains two push buttons: OK and Cancel.
MB_RETRYCANCEL=0x00000005#The message box contains two push buttons: Retry and Cancel.
MB_YESNO=0x00000004#The message box contains two push buttons: Yes and No.
MB_YESNOCANCEL=0x00000003#The message box contains three push buttons: Yes, No, and 

MB_TOPMOST=0x00040000#The message box is created with the WS_EX_TOPMOST window style.
IDABORT=3#The Abort button was selected.
IDCANCEL=2#The Cancel button was selected.
IDCONTINUE=11#The Continue button was selected.
IDIGNORE=5#The Ignore button was selected.
IDNO=7#The No button was selected.
IDOK=1#The OK button was selected.
IDRETRY=4#The Retry button was selected.
IDTRYAGAIN=10#The Try Again button was selected.
IDYES=6#The Yes button was selected.


MessageBox = ctypes.windll.user32.MessageBoxW
res=MessageBox(HWND, '本当に色付き出力にしますか?色付きにするとすごく時間がかかります。', '色付き出力選択', MB_TOPMOST|MB_YESNOCANCEL)

2022年6月26日日曜日

post_main()プロセスの分離

 PythonのC API上の問題らしいのですが、PythonInterpreterを2回呼ぶ場合問題があるらしいです。実際、下で述べられている現象と似たような事態は、大きなモジュールPywin32を導入時に経験しました。

Python C API error restarting interpreter - Stack Overflow

あれこれ対策してみたのですが、どうしても対策できなくて2回呼ぶのを諦めました。正確には、同じプロセスで2回呼ぶのをではなく、プロセスを2回用意して各1回づつ呼ぶ仕様に変更しました。プロセスが別であれば、Python側のメモリも新しいメモリになり、Python2回問題を回避できます。幸い、一回目は、制約論理生成用、2回目は、post_main() 出力データの整形用、と明確に用途が分かれています。制約論理生成と、出力データ整形は、全く別物であり独立化出来ます。なのでプロセスを分離してもなんら問題を生じません。



2022年6月25日土曜日

CSV TASK Format変更

 「Shift出力では、日付(day)表示付きなのに、Task出力では、Day表示がない」という

ご指摘にもとづき、155Aよりフォーマット変更しています。今まで空欄だったところにDay表示が埋まるだけなので、副作用はないと思いますが、問題のある方はお知らせください。



2022年6月23日木曜日

Pywin32 データWriteの高速化

 Cell.Valueだけでしたら可能です。PythonでRangeを読んでみると2次元Tupleで返ってきました。つまり2次元配列で書いてやれば、時間のかかるCOM通信を一回で行えるのではないか?と推察しました。それに対して、Interior.Colorとかは、明示的な構造体・配列を示唆する情報が返ってきません。(print(xx)で見ました。)

なので、とりあえず、Valueは行けるのではないかと思って書いたのが次のソースです。これで、体感数十倍速にはなりました。

残念なことにこの技は、Interior.Colorには使えません。Rangeで一括は、定数では可能です。ですから初期化には使えます。例えば,次のソースです。

ws.UsedRange.Interior.ColorIndex=0 #Clear Colors

VBAでは、UnionでプログラムでRangeに追加できますが、Pythonではそうしたことが可能になる例は見つけられませんでした。やむを得ず、色付の場合、一個ずづ処理するので時間がかかりますよ。いいですか? のメッセージを出すのが精一杯ということになります。(無理やりPythonでやろうとすれば、Pythonソースをダイナミックに生成することになると思いますが..)

さて、求解すると次のところで停止します。pdb.set_trace()の次の行で停止します。

デバッグしたい開始点に、この行を設置することになります。ソースエディタで設定したブレークポイントは、set_traceで読み込まれます。また、当該エディタ上で停止しているときに設置・解除することが出来ます。実行ボタンを押すと進みます。


次の画像は、時間がかかるので、どうします?と聞いてくるスナップショットです。
このプロジェクトは、INRC2 35人4WEEKSです。

色付出力を選択した場合、90秒 


色なしを選択した場合、2-3秒


なお、プロジェクトファイル名と同名の拡張子xlsxのExcelファイルが無ければフォーマットから開始するソースです。

<不明なCOMエラーが出る場合>
ソース行で、ExcelをVisibleにしておくことは重要です。

xl.Visible = True#False makes debug harder..

FalseにするとExcelが現れません。で、プログラム開発途中でExcelがプロセスとして残っていたりすると、そのExcelに対して操作が行われます。がExcelそのものは見えないと、原因不明のCOMエラーになったりします。Excelが現れていれば、その辺は、すぐに分かります。Excelが現れて、余計なキー操作でエラーになったりすることはありますが、その事自体は気付けるので、程度が良いです。プロセスが残っているかどうかは、タスクマネージャで見ることが出来ます。起動していないのに存在する!、という場合は、死んでもらってから求解するようにしましょう。

以下、Pythonソースです。ScheduleNurse固有命令sc3...は一切使っていませんが、GUIの集合定義は参照しています。これ位の規模になるとデバッガが欲しくなった次第です。最初は、print()文の実装で済まそうと思っていたのが、やはり欲しくなって155Aから内蔵しています。


import os
import datetime
import locale
import ctypes
import win32gui
import win32com.client
import re
import sc3
import pdb

#---------------------------
xlHairline = 1
xlThin     = 2
xlThick    = 4
xlMedium   = -4138

    # ------------------------------------------------------------------
    # Excel Enum XlFileFormat
    # ------------------------------------------------------------------
xlCSV                         =  6
xlHtml                        = 44
xlWorkbookDefault             = 51
xlOpenXMLWorkbook             = 51
xlOpenXMLWorkbookMacroEnabled = 52
xlWorkbookNormal              = -4143
xlCurrentPlatformText         = -4158

    # ------------------------------------------------------------------
    # Excel Enum XlLineStyle
    # ------------------------------------------------------------------
xlContinuous    =  1
xlDashDot       =  4
xlDashDotDot    =  5
xlSlantDashDot  = 13
xlDash          = -4115
xldot           = -4118
xlDouble        = -4119
xlLineStyleNone = -4142
    # ------------------------------------------------------------------
    # Excel Enum XlOrientation
    # ------------------------------------------------------------------
xlHorizontal = -4128
xlVertical   = -4166
xlDownward   = -4170
xlUpward     = -4171
 
# ------------------------------------------------------------------
#https://docs.microsoft.com/en-us/office/vba/api/excel.xlformatconditionoperator
xlCellValue=1
xlExpression=2
xlBetween=1#	Between. Can be used only if two formulas are provided.
xlEqual=3#	Equal.
xlGreater=5#	Greater than.
xlGreaterEqual=7#	Greater than or equal to.
xlLess=6#	Less than.
xlLessEqual=8#	Less than or equal to.
xlNotBetween=2#	Not between. Can be used only if two formulas are provided.
xlNotEqual=4#	Not equal.
xlUnderlineStyleSingle           = 2 
xlCenter      = -4108

MB_OK=0x00000000#The message box contains one push button: OK. This is the default.
MB_OKCANCEL=0x00000001#The message box contains two push buttons: OK and Cancel.
MB_RETRYCANCEL=0x00000005#The message box contains two push buttons: Retry and Cancel.
MB_YESNO=0x00000004#The message box contains two push buttons: Yes and No.
MB_YESNOCANCEL=0x00000003#The message box contains three push buttons: Yes, No, and 

MB_TOPMOST=0x00040000#The message box is created with the WS_EX_TOPMOST window style.
IDABORT=3#The Abort button was selected.
IDCANCEL=2#The Cancel button was selected.
IDCONTINUE=11#The Continue button was selected.
IDIGNORE=5#The Ignore button was selected.
IDNO=7#The No button was selected.
IDOK=1#The OK button was selected.
IDRETRY=4#The Retry button was selected.
IDTRYAGAIN=10#The Try Again button was selected.
IDYES=6#The Yes button was selected.

def post_main():
    pdb.set_trace()
    
    print(locale.getlocale(locale.LC_CTYPE))
    print("Hello post main world!")
    print('\n\n*********Processing Post operation *************\n')
    phases=len(dayphase_list)
#
#Launch Excel
    try :
        xl = win32com.client.Dispatch("Excel.Application")
    except:
        print("can not invoke excel")
        exit()
    #Show working
    xl.Visible = True#False makes debug harder..
    os.chdir(project_file_path)
    file_name=project_file_name+".xlsx";
    file=os.path.join(project_file_path,file_name)
    exists=os.path.exists(file) 
    if exists:
        wb = xl.Workbooks.Open(file)
    else:
        wb=xl.Workbooks.Add()
    
    
    
    # Excelsheet object
    ws = wb.Worksheets(1)
    with_color=False
    if not exists:
        day_str=daydef[今月[0]]
        day=int(day_str[8:])#Day
        one_month_mode=False
        if day==1 and len(今月)<=31:
            one_month_mode=True
        make_initial_format_improved(ws,one_month_mode)
    
    
    sc_hwnd=get_sc3_handle()
    print("hwnd=",sc_hwnd)
 
    if "Japan" in locale.getlocale(locale.LC_CTYPE)[0]:#('Japanese_Japan', '932')('English_United States', '932')
        MessageBox = ctypes.windll.user32.MessageBoxW
        res=MessageBox(sc3_hwnd, '本当に色付き出力にしますか?色付きにするとすごく時間がかかります。', '色付き出力選択', MB_TOPMOST|MB_YESNOCANCEL)
    else:
        MessageBox = ctypes.windll.user32.MessageBoxW
        res=MessageBox(sc3_hwnd, 'Are sure you want the results with colored?\nIt takes far longer time with colored.', 'With/Without Color Selection',MB_TOPMOST|MB_YESNOCANCEL)
        
    if res==IDYES:#
        with_color=True
    elif res==IDCANCEL:#Cancel
        wb.SaveAs(f"{os.getcwd()}\\"+file_name, FileFormat = xlOpenXMLWorkbook)
        wb.Close(True)# True:Save False=Default:Not Save
        xl.Quit()
        return
    else:#No
        with_color=False
    make_data_improved(ws,phases,with_color)#Output the SC3 Data on the formatted sheet.
    if not exists:
        wb.SaveAs(f"{os.getcwd()}\\"+file_name, FileFormat = xlOpenXMLWorkbook)
    
    
    wb.Close(True)# True:Save False=Default:Not Save
    # Excel Quit
    xl.Quit()
    print('\n\n*********Processed Post Operation.*************\n')

def clear(ws):
    ws.UsedRange.ClearContents()
    ws.UsedRange.ClearFormats()
    

def set_font(ws,addr,value):
    ws.Range(addr).Value=value
    ws.Range(addr).Font.Name = "Yu Gothic UI"
    # Set Cell font size
    ws.Range(addr).Font.Size = 14
    # Set Bold
    ws.Range(addr).Font.Bold = True


def make_initial_format_improved(ws,one_month_mode=False):
    if one_month_mode==True:
        print("one_month mode formatting..")
        day_str=daydef[今月[0]]
        day=int(day_str[8:])#Day
        if day !=1 :
            print("Fatal Error. 今月 must start with 1.")
            exit()
        if len(今月)>31:
            print("Fatal Error. Excessive 今月 Length.")
            exit()
    else:
        print("nominal mode formatting..")
    今月2=list(今月)#Deep Copy
    if one_month_mode==True:
        for m in range(31)[len(今月):]:
            今月2.append(m+今月[0])

    clear(ws)
    
    set_font(ws,"D2","Work Schedule")
    set_font(ws,"AW2","")
    #ws.Range("AW2").Formula='=Year(E7)'
    ws.Range("AW2","AY2").Merge()
    ws.Range("AW2").Font.Underline = xlUnderlineStyleSingle
    ws.Range("AW1","AY1").Merge()
    set_font(ws,"AW1",'Year')
    
    #ws.Range("AZ2").Formula='=Month(E7)'
    ws.Range("AZ2").Font.Underline = xlUnderlineStyleSingle
    ws.Range("AZ2","BB2").Merge()
    set_font(ws,"AZ1",'Month')
    ws.Range("AZ2","BB2").Merge()

    #ws.Range("BC2").Formula='=Month(E7)'
    ws.Range("BC2").Font.Underline = xlUnderlineStyleSingle
    ws.Range("BC2","BD2").Merge()
    set_font(ws,"BC1",'Day')
    ws.Range("BC1","BD1").Merge()

    ws.Columns(2).EntireColumn.ColumnWidth = 20
    ws.Columns(3).EntireColumn.ColumnWidth = 10
    ws.Columns(4).EntireColumn.ColumnWidth = 5

    row_start=8
    row=row_start
    col_start=2
    col=col_start
    day_col_offset=3
    persons=len(staffdef)
    days=len(今月2)
    phases=len(dayphase_list)
    cols=phases*days
    day_processed=0
    row=row_start-2

    #Draw Top Header
    for day in 今月2:
        col=col_start+day_col_offset+day_processed
        for ph in range(phases):
            ws.Columns(col+ph).EntireColumn.ColumnWidth = 2

        if day_processed==0:
            day_str=daydef[day]
        
            #print(day_str,int(day_str[8:]))
            ws.Range("AW2").Formula='='+day_str[0:4]#Year
            ws.Range("AZ2").Formula='='+day_str[5:7]#Month
            ws.Range("BC2").Formula='='+day_str[8:]#Day
               
            
            print(get_relative_addr_str(ws,row-1,col))
            ws.Cells(row-1,col).Formula='=Date(AW2,AZ2,BC2)'#
            ws.Cells(row  ,col).Formula='=Day('+get_relative_addr_str(ws,row-1,col)+')' 
            
            day_color_format2(ws,row,col,-1)
            merge_and_align(ws,row ,col,row,col+phases-1)
            
            ws.Cells(row+1  ,col).Formula='='+get_relative_addr_str(ws,row-1,col)   
            ws.Cells(row+1,col).NumberFormatLocal='aaa'
            day_color_format2(ws,row+1,col,-2)
            merge_and_align(ws,row+1,col,row+1,col+phases-1)
            
        elif day_processed==phases:
            ws.Cells(row-1,col).Formula='='+get_relative_addr_str(ws,row-1,col-phases)+'+1'
            if one_month_mode:
                ws.Cells(row  ,col).Formula='=if($AZ$2 <> Month('+get_relative_addr_str(ws,row-1,col)+'),"",Day('+get_relative_addr_str(ws,row-1,col)+'))'
            else:
                ws.Cells(row  ,col).Formula='=Day('+get_relative_addr_str(ws,row-1,col)+')' 
            day_color_format2(ws,row,col,-1,one_month_mode)
            merge_and_align(ws,row ,col,row,col+phases-1)
            
            if one_month_mode:
                #print("")
                ws.Cells(row+1  ,col).Formula='=if($AZ$2 <> Month('+get_relative_addr_str(ws,row-1,col)+'),"",'+get_relative_addr_str(ws,row-1,col)+')'
            else:
                ws.Cells(row+1  ,col).Formula='='+get_relative_addr_str(ws,row-1,col) #'=Day('+get_relative_addr_str(ws,row-1,col)+')'   
            ws.Cells(row+1,col).NumberFormatLocal='aaa'
            day_color_format2(ws,row+1,col,-2,one_month_mode)
            merge_and_align(ws,row+1,col,row+1,col+phases-1)
            
            
        else:
            ws.Range(ws.Cells(row-1,col-phases),ws.Cells(row-1,col-1)).Copy(ws.Range(ws.Cells(row-1,col),ws.Cells(row-1,col)))
            ws.Range(ws.Cells(row,col-phases),ws.Cells(row+1,col-1)).Copy(ws.Range(ws.Cells(row,col),ws.Cells(row,col)))
        day_processed +=phases

    #Draw Person Property and Data Area
    row=row_start
    for person in All_Staff:
        day_processed=0
        row =row_start+person*4
        if person==All_Staff[0]:
            #name=staffdef[person]
            #ws.Cells(row,col_start).Value=name   
            col=col_start
            merge_and_align(ws,row,col,row+3,col)
            ws.Cells(row,col+1).Value='Preferred'
            merge_and_align(ws,row,col+1,row+1,col+1)

            ws.Cells(row+2,col+1).Value='Sc3 Solution'
            merge_and_align(ws,row+2,col+1,row+3,col+1)

            ws.Cells(row,col+2).Value='Shift'
            merge_and_align(ws,row,col+2,row,col+2)

            ws.Cells(row+2,col+2).Value='Shift'
            merge_and_align(ws,row+2,col+2,row+2,col+2)
        
            ws.Cells(row+1,col+2).Value='Task'
            merge_and_align(ws,row+1,col+2,row+1,col+2)
        
            ws.Cells(row+3,col+2).Value='Task'
            merge_and_align(ws,row+3,col+2,row+3,col+2)
            
            for day in 今月2:
                #print("row=",row,"row_start=",row_start)
                col=col_start+day_col_offset+day_processed
                merge_and_align(ws,row,col,row,col+phases-1)
                merge_and_align(ws,row+2,col,row+2,col+phases-1)
                ws.Range(ws.Cells(row,col),ws.Cells(row+3,col+phases-1)).Borders.LineStyle = xlContinuous
                ws.Range(ws.Cells(row,col),ws.Cells(row+3,col+phases-1)).Borders.weight=xlThin
                day_processed +=phases
        else:
            data_start_col=col_start
            data_end_col=data_start_col+day_col_offset+phases*len(今月2)-1
            ws.Range(ws.Cells(row-4,data_start_col),ws.Cells(row-1,data_end_col)).Copy(ws.Range(ws.Cells(row,data_start_col),ws.Cells(row,data_start_col)))

    #Fill in Staff names
    list2 = [['' for i in range(1)] for j in range(len(All_Staff)*4)]#Constructor
    #wk=ws.Range(ws.Cells(row_start,col_start),ws.Cells(row_start+4*len(All_Staff)-1,col_start)).Value#Values ->2 dim array
    print("list2=",len(list2))
    #[[],[],[]...
    for person in All_Staff:
        list2[person*4][0]=staffdef[person]
    ws.Range(ws.Cells(row_start,col_start),ws.Cells(row_start+4*len(All_Staff)-1,col_start)).Value=list2#Restore it    
    ws.Rows(5).EntireRow.Hidden = True#Hide intemidiate Results

def get_relative_addr_str(ws,row,col):
    str=ws.Cells(row,col).address
    str=str.replace('$','')
    return str
def day_color_format2(ws,row,col,row_offset,one_month_mode=False):
    ws.Cells(row,col).FormatConditions.Delete()#Add(xlCellValue, xlGreaterEqual, '=85')

    if one_month_mode:
        ws.Cells(row,col).FormatConditions.Add(xlExpression, xlGreater,'=Month('+get_relative_addr_str(ws,row+row_offset,col)+')>$AZ$2')
        ws.Cells(row,col).FormatConditions.Add(xlExpression, xlEqual, '=Weekday('+get_relative_addr_str(ws,row+row_offset,col)+')=7') #SAT:7 Sun:1
        ws.Cells(row,col).FormatConditions.Add(xlExpression, xlEqual, '=Weekday('+get_relative_addr_str(ws,row+row_offset,col)+')=1') #SAT:7 Sun:1
        
        ws.Cells(row,col).FormatConditions(1).Interior.ColorIndex=2 #White
        ws.Cells(row,col).FormatConditions(2).Interior.ColorIndex=20 #LightBlue
        ws.Cells(row,col).FormatConditions(3).Interior.ColorIndex=22 #
    else:
        ws.Cells(row,col).FormatConditions.Add(xlExpression, xlEqual, '=Weekday('+get_relative_addr_str(ws,row+row_offset,col)+')=7') #SAT:7 Sun:1
        ws.Cells(row,col).FormatConditions.Add(xlExpression, xlEqual, '=Weekday('+get_relative_addr_str(ws,row+row_offset,col)+')=1') #SAT:7 Sun:1
        
        ws.Cells(row,col).FormatConditions(1).Interior.ColorIndex=20 #LightBlue
        ws.Cells(row,col).FormatConditions(2).Interior.ColorIndex=22 #
def merge_and_align(ws,row_start1,col_start1,row_end1,col_end1,weight=xlThin):
        

        print(row_start1,col_start1,row_end1,col_end1)

        ws.Range(ws.Cells(row_start1,col_start1), ws.Cells(row_end1,col_end1)).Merge()
        ws.Range(ws.Cells(row_start1,col_start1), ws.Cells(row_end1,col_end1)).VerticalAlignment = xlCenter
        ws.Range(ws.Cells(row_start1,col_start1), ws.Cells(row_end1,col_end1)).HorizontalAlignment = xlCenter
        ws.Range(ws.Cells(row_start1,col_start1), ws.Cells(row_end1,col_end1)).Borders.LineStyle = xlContinuous
        ws.Range(ws.Cells(row_start1,col_start1), ws.Cells(row_end1,col_end1)).Borders.Weight = weight

#Sub rountines



def get_shift_label(shift,scheduled_shift_label):
    for key in shift_label_color_map.keys():
        if key ==shift:
            item_array=shift_label_color_map[key]
            i=0
            for item in item_array:
                if i==0:
                    solution_shift_label=item[0]
                    solution_label_color=item[1]
                else:
                    if len(scheduled_shift_label)>=1 and item[0]==scheduled_shift_label:
                        return item[0],item[1]
                i+=1
            return solution_shift_label,solution_label_color
    print("Fatal Error",shift,scheduled_shift_label)
    exit()

def get_task_label(task):
    
    if task in  task_label_color_map:
        return task_label_color_map[task][0][0]
    else:
        print("Invalid Task", task)
        exit()
def rgb_to_hex(rgb):
    '''
    ws.Cells(1, i).Interior.color uses bgr in hex

    '''
    bgr = (int(rgb[2]),int(rgb[1]),int(rgb[0]))
    strValue = '%02x%02x%02x' % bgr
    # print(strValue)
    iValue = int(strValue, 16)
    return iValue

def get_task_label_color(task_label):
    if len(task_label)==0 or task_label=='.':
        return 0xffffff #空白,Dummy DummyDay
    for item_array in task_label_color_map.values():#task集合から探して
        for item in item_array:#taskは複数のラベルを持つ
            task_label_candidate=item[0]
            if task_label==task_label_candidate:#ラベル名が一致したなら
                color_str=item[1]
                rgb = re.findall(r"\d+",color_str)
                if len(rgb)==3:
                    return rgb_to_hex(rgb)
                else:
                    return 0xffffff #rgb形式でなければWhiteを返す
    for item in task_collections_def.values():#無かったらtask aggregatesから探す
        task_label_candidate=item[0]
        #if len(task_label)>=1:
        #    print("list",task_label,task_label_candidate)
        if task_label==task_label_candidate:
            #print("matched",task_label_candidate)
            color_str=item[1]
            rgb = re.findall(r"\d+",color_str)
            if len(rgb)==3:
                return rgb_to_hex(rgb)
            else:
                return 0xffffff #rgb形式でなければWhiteを返す
    #print("unmatched",task_label)
    return 0xffffff #なければWhiteを返す

def get_shift_label_color(shift_label):
    if len(shift_label)==0 or shift_label=='・':
        return 0xffffff #空白ならWhiteを返す
    for item_array in shift_label_color_map.values():#task集合から探して
        for item in item_array:#taskは複数のラベルを持つ
            shift_label_candidate=item[0]
            #print("label ",shift_label,shift_label_candidate)
            if shift_label==shift_label_candidate:#ラベル名が一致したなら
                color_str=item[1]
                rgb = re.findall(r"\d+",color_str)
                if len(rgb)==3:
                    return rgb_to_hex(rgb)
                else:
                    return 0xffffff #rgb形式でなければWhiteを返す
    #print(shift_label," line511")
    for item in shift_collections_def.values():#無かったらtask aggregatesから探す
        shift_label_candidate=item[0]
        #if len(task_label)>=1:
        #    print("list",task_label,task_label_candidate)
        if shift_label==shift_label_candidate:
            #print("matched",task_label_candidate)
            color_str=item[1]
            rgb = re.findall(r"\d+",color_str)
            if len(rgb)==3:
                return rgb_to_hex(rgb)
            else:
                return 0xffffff #rgb形式でなければWhiteを返す
    for item in phase_objects_def.values():
        shift_label_candidate=item[0]
        if shift_label==shift_label_candidate:
            color_str=item[1]
            rgb = re.findall(r"\d+",color_str)
            if len(rgb)==3:
                return rgb_to_hex(rgb)
            else:
                return 0xffffff #rgb形式でなければWhiteを返す
    for item in phase_aggregate_object_def.values():
        shift_label_candidate=item[0]
        if shift_label==shift_label_candidate:
            color_str=item[1]
            rgb = re.findall(r"\d+",color_str)
            if len(rgb)==3:
                return rgb_to_hex(rgb)
            else:
                return 0xffffff 
    #print("unmatched",task_label)
    return 0xffffff #なければWhiteを返す  
def get_task_color(task):
    if len(task)==0 or task=='Dummy' or task=='DummyDay':
        return 0xffffff
    if task in  task_label_color_map:
        color_str=task_label_color_map[task][0][1]
        rgb = re.findall(r"\d+",color_str)
        if len(rgb)==3:
            return rgb_to_hex(rgb)
        else:
            return 255 + 255*256 + 255*256*256
    else:
        return 255 + 255*256 + 255*256*256


def rgb_str_to_hex(color_str):
    rgb = re.findall(r"\d+",color_str)
    if len(rgb)==3:
        return rgb_to_hex(rgb)
    else:
        return 0xffffff
def make_data_improved(ws,phases,color_enabled=False):
    ws.UsedRange.Interior.ColorIndex=0 #Clear Colors
    day_cols=phases*len(今月)
    person_rows=4*len(All_Staff)
    list2_val = [['' for i in range(day_cols)] for j in range(person_rows)]#2 Dim Array Constructor for shift/task
    list2_color = [[0xfffffff for i in range(day_cols)] for j in range(person_rows)]#2 Dim Array Constructor for label color Default White(0xffffff)

    for person in All_Staff:
        staff_name=staffdef[person]
        c=ws.Range("B1:B151").Find(staff_name)
        print('Excel Processing Data ',staff_name,c.Row,c.Column)
        col_offset=3
        if person==0:
            row_start=c.Row
            col_start=c.Column+col_offset
        for day in 今月:
            D=day-制約開始日
            #print(day)
            tph=D*phases
            #print("day",day)
            shift_label=shift_schedules[person][day][2]
            #ws.Cells(c.Row,c.Column+3+col*phases).Value=shift_label#Scheduled Shift
            list2_val[person*4][tph]=shift_label
            shift=shift_schedules[person][day][0]
            shift_color=get_shift_label_color(shift_label)    
            #ws.Cells(c.Row,c.Column+3+col*phases).Interior.Color=shift_color#Scheduled Shift Color
            list2_color[person*4][tph]=shift_color
            for m in range(phases):
                tl=task_schedules[person][day*phases+m][2]#label
                tl_color=get_task_label_color(tl)
                #ws.Cells(c.Row+1,c.Column+3+tph+m).Value=tl#task schedule
                #ws.Cells(c.Row+1,c.Column+3+tph+m).Interior.Color=tl_color
                list2_val  [person*4+1][tph+m]  =tl
                list2_color[person*4+1][tph+m]  =tl_color
  
            shift=shift_solution[person][day]
 
            label_color=get_shift_label(shift,shift_label)#return (label,color_str)
            #ws.Cells(c.Row+2,c.Column+3+col*phases).Value=label_color[0]
            #ws.Cells(c.Row+2,c.Column+3+col*phases).Interior.Color=rgb_str_to_hex(label_color[1])
            list2_val[person*4+2][tph]  =label_color[0]
            list2_color[person*4+2][tph]=rgb_str_to_hex(label_color[1])
  
            for m in range(phases):
                task0=task_solution[person][day*phases+m]
                if len(task0)>=1:
                    t0_color=get_task_color(task0)
                    #ws.Cells(c.Row+3,c.Column+3+tph+m).Interior.Color=t0_color
                    list2_color[person*4+3][tph+m]=t0_color
                    task_label=get_task_label(task0)
                    #ws.Cells(c.Row+3,c.Column+3+tph+m).Value=task_label
                    list2_val[person*4+3][tph+m]=task_label
                #else:
                    #ws.Cells(c.Row+3,c.Column+3+tph+m).Interior.Color=0xffffff
                    #ws.Cells(c.Row+3,c.Column+3+tph+m).Value=""
    col_end=col_start+len(今月)*phases-1
    ws.Range(ws.Cells(row_start,col_start),ws.Cells(row_start+4*len(All_Staff)-1,col_end)).Value=list2_val
    #wk=ws.Range(ws.Cells(row_start,col_start),ws.Cells(row_start+4*len(All_Staff)-1,col_end)).Interior.Color#=list2_color
    #print(len(wk))
    if color_enabled:
        print("Please be patient for 2 min.")
        for row_person in range(len(list2_color)):
            for day_col in range(len(list2_color[row_person])):
                if list2_color[row_person][day_col] !=0xffffff:
                    row=row_start+row_person
                    col=col_start+day_col
                    ws.Cells(row,col).Interior.Color=list2_color[row_person][day_col]
def get_sc3_handle():
    global sc3_hwnd
    win32gui.EnumWindows( winEnumHandler, None )
    sc3_hwnd=int(int(sc3_hwnd,0))
    return sc3_hwnd
def winEnumHandler( hwnd, ctx ):
    if win32gui.IsWindowVisible( hwnd ):
        text=win32gui.GetWindowText( hwnd )
        print (hex(hwnd),text )
        if ('スケジュールナース' in text or "Schedule Nurse" in text) and project_file_name in text:
            global sc3_hwnd
            sc3_hwnd=hex(hwnd)
            print("Detected",sc3_hwnd)
   


Pywin32 ダイアログを前面に出したい改善

 スケジュールナースGUIは、複数画面の存在を許しています。(内部ソルバーは、一個しか許していないので求解を同時に行うことは出来ませんがソフトを複数起動することは可能です。)なので、前回のコードでは、複数のスケジュールナースを検出してしまう可能性があります。

幸い、次のリリースから、project_file_nameという変数にプロジェクトファイル名を入れているので、これをANDに取れば、複数の検出は防げます。



import win32com.client
import win32gui
import ctypes

MB_TOPMOST=0x00040000
MB_YESNOCANCEL=0x00000003
def winEnumHandler( hwnd, ctx ):
    if win32gui.IsWindowVisible( hwnd ):
        text=win32gui.GetWindowText( hwnd )
        print (hex(hwnd),text )
        if ('スケジュールナース' in text or "Schedule Nurse" in text) and project_file_name in text:
            global sc3_hwnd
            sc3_hwnd=hex(hwnd)
            print("Detected",sc3_hwnd)
sc3_hwnd=0
win32gui.EnumWindows( winEnumHandler, None )
MessageBox = ctypes.windll.user32.MessageBoxW
hwnd=int(int(sc3_hwnd,0))
print("hwnd=",hwnd)
#res=MessageBox(0, '本当に色付き出力にしますか?色付きにするとすごく時間がかかります。', '色付き出力選択',MB_TOPMOST|MB_YESNOCANCEL)
res=MessageBox(hwnd, '本当に色付き出力にしますか?色付きにするとすごく時間がかかります。', '色付き出力選択',MB_TOPMOST|MB_YESNOCANCEL)
        

2022年6月22日水曜日

Pywin32 ダイアログを前面に出したい

 前回までフォーマットを行いましたが、今度は、そのフォーマット上に、解データを書いていきます。問題は、書く時間ですが、色なしにすれば、2-3秒に改善出力が可能なことが分かりました。ところが、スケジュールナースⅢで定義したラベルの色付でだそうとすると、

色付:90秒

となりました。なので、次のようなダイアログを出して選択を促そうと思いました。


ところが、このダイアログが背面に隠れてしまって出てこないときがあります。(連続で出てくることもあったり、連続で背面に行くことがあったりします。)

こうなるとハングしたように思われるので致命的です。そこでダイアログを前面にする方法を検討しました。TKInterも検討しましたが、上手くいきませんでした。

python ctypes messagebox appears under all programs - Stack Overflow

ということで、スケジュールナースのWindowsHandleが得られれば上手く行きそうだということが分かりました。(Win32APIそのままです。)

Window Handleを得る方法は、

python - How to get a list of the name of every open window? - Stack Overflow

ということです。win32guiをインポート出来れば良いのですが、幸いなことにPywin32に含まれていました。

で、次のコードでダイアログを前面に出すことができました。EnumWindowsは値を返せないので、やむを得ずglobalを使用しました。



import win32com.client
import win32gui
import ctypes



MB_TOPMOST=0x00040000
MB_YESNOCANCEL=0x00000003
def winEnumHandler( hwnd, ctx ):
    if win32gui.IsWindowVisible( hwnd ):
        text=win32gui.GetWindowText( hwnd )
        print (hex(hwnd),text )
        if 'スケジュールナース' in text or "Schedule Nurse" in text:
            global sc3_hwnd
            sc3_hwnd=hex(hwnd)
            print("Detected",sc3_hwnd)
sc3_hwnd=0
win32gui.EnumWindows( winEnumHandler, None )
MessageBox = ctypes.windll.user32.MessageBoxW
hwnd=int(int(sc3_hwnd,0))
print("hwnd=",hwnd)
#res=MessageBox(0, '本当に色付き出力にしますか?色付きにするとすごく時間がかかります。', '色付き出力選択',MB_TOPMOST|MB_YESNOCANCEL)
res=MessageBox(hwnd, '本当に色付き出力にしますか?色付きにするとすごく時間がかかります。', '色付き出力選択',MB_TOPMOST|MB_YESNOCANCEL)
        

print("sc3_hwnd",sc3_hwnd)

2022年6月21日火曜日

Pywin32によるExcel Format操作その3

 4週28日固定ではなく、月毎の勤務表の場合、たとえば2月29日がない場合もありえます。そういう場合のフォーマット対応も行いました。


ソース上では、one_month_modeで前回のコードに追加修正しています。

最初のDayの日の月と、当該セルの月が違ったら、ブランクにしたり背景色を白にしたり、という処理を追加で記述しています。

それとは別に、Pythonでハマった点は、listの代入は、参照コピーということです。

今月2=list(今月) で値コピーにしています。



def make_initial_format_improved(ws,one_month_mode=False):
    if one_month_mode==True:
        print("one_month mode formatting..")
        day_str=daydef[今月[0]]
        day=int(day_str[8:])#Day
        if day !=1 :
            print("Fatal Error. 今月 must start with 1.")
            exit()
        if len(今月)>31:
            print("Fatal Error. Excessive 今月 Length.")
            exit()
    else:
        print("nominal mode formatting..")
    今月2=list(今月)#Deep Copy
    if one_month_mode==True:
        for m in range(31)[len(今月):]:
            今月2.append(m+今月[0])

    clear(ws)
    
    set_font(ws,"D2","Work Schedule")
    set_font(ws,"AW2","")
    #ws.Range("AW2").Formula='=Year(E7)'
    ws.Range("AW2","AY2").Merge()
    ws.Range("AW2").Font.Underline = xlUnderlineStyleSingle
    ws.Range("AW1","AY1").Merge()
    set_font(ws,"AW1",'Year')
    
    #ws.Range("AZ2").Formula='=Month(E7)'
    ws.Range("AZ2").Font.Underline = xlUnderlineStyleSingle
    ws.Range("AZ2","BB2").Merge()
    set_font(ws,"AZ1",'Month')
    ws.Range("AZ2","BB2").Merge()

    #ws.Range("BC2").Formula='=Month(E7)'
    ws.Range("BC2").Font.Underline = xlUnderlineStyleSingle
    ws.Range("BC2","BD2").Merge()
    set_font(ws,"BC1",'Day')
    ws.Range("BC1","BD1").Merge()

    ws.Columns(2).EntireColumn.ColumnWidth = 20
    ws.Columns(3).EntireColumn.ColumnWidth = 10
    ws.Columns(4).EntireColumn.ColumnWidth = 5

    row_start=8
    row=row_start
    col_start=2
    col=col_start
    day_col_offset=3
    persons=len(staffdef)
    days=len(今月2)
    phases=len(dayphase_list)
    cols=phases*days
    day_processed=0
    row=row_start-2

    #Draw Top Header
    for day in 今月2:
        col=col_start+day_col_offset+day_processed
        for ph in range(phases):
            ws.Columns(col+ph).EntireColumn.ColumnWidth = 2

        if day_processed==0:
            day_str=daydef[day]
        
            #print(day_str,int(day_str[8:]))
            ws.Range("AW2").Formula='='+day_str[0:4]#Year
            ws.Range("AZ2").Formula='='+day_str[5:7]#Month
            ws.Range("BC2").Formula='='+day_str[8:]#Day
               
            
            print(get_relative_addr_str(ws,row-1,col))
            ws.Cells(row-1,col).Formula='=Date(AW2,AZ2,BC2)'#
            ws.Cells(row  ,col).Formula='=Day('+get_relative_addr_str(ws,row-1,col)+')' 
            
            day_color_format2(ws,row,col,-1)
            merge_and_align(ws,row ,col,row,col+phases-1)
            
            ws.Cells(row+1  ,col).Formula='='+get_relative_addr_str(ws,row-1,col)   
            ws.Cells(row+1,col).NumberFormatLocal='aaa'
            day_color_format2(ws,row+1,col,-2)
            merge_and_align(ws,row+1,col,row+1,col+phases-1)
            
        elif day_processed==phases:
            ws.Cells(row-1,col).Formula='='+get_relative_addr_str(ws,row-1,col-phases)+'+1'
            if one_month_mode:
                ws.Cells(row  ,col).Formula='=if($AZ$2 <> Month('+get_relative_addr_str(ws,row-1,col)+'),"",Day('+get_relative_addr_str(ws,row-1,col)+'))'
            else:
                ws.Cells(row  ,col).Formula='=Day('+get_relative_addr_str(ws,row-1,col)+')' 
            day_color_format2(ws,row,col,-1,one_month_mode)
            merge_and_align(ws,row ,col,row,col+phases-1)
            
            if one_month_mode:
                #print("")
                ws.Cells(row+1  ,col).Formula='=if($AZ$2 <> Month('+get_relative_addr_str(ws,row-1,col)+'),"",'+get_relative_addr_str(ws,row-1,col)+')'
            else:
                ws.Cells(row+1  ,col).Formula='='+get_relative_addr_str(ws,row-1,col) #'=Day('+get_relative_addr_str(ws,row-1,col)+')'   
            ws.Cells(row+1,col).NumberFormatLocal='aaa'
            day_color_format2(ws,row+1,col,-2,one_month_mode)
            merge_and_align(ws,row+1,col,row+1,col+phases-1)
            
            
        else:
            ws.Range(ws.Cells(row-1,col-phases),ws.Cells(row-1,col-1)).Copy(ws.Range(ws.Cells(row-1,col),ws.Cells(row-1,col)))
            ws.Range(ws.Cells(row,col-phases),ws.Cells(row+1,col-1)).Copy(ws.Range(ws.Cells(row,col),ws.Cells(row,col)))
        day_processed +=phases

    #Draw Person Property and Data Area
    row=row_start
    for person in All_Staff:
        day_processed=0
        row =row_start+person*4
        if person==All_Staff[0]:
            #name=staffdef[person]
            #ws.Cells(row,col_start).Value=name   
            col=col_start
            merge_and_align(ws,row,col,row+3,col)
            ws.Cells(row,col+1).Value='Preferred'
            merge_and_align(ws,row,col+1,row+1,col+1)

            ws.Cells(row+2,col+1).Value='Sc3 Solution'
            merge_and_align(ws,row+2,col+1,row+3,col+1)

            ws.Cells(row,col+2).Value='Shift'
            merge_and_align(ws,row,col+2,row,col+2)

            ws.Cells(row+2,col+2).Value='Shift'
            merge_and_align(ws,row+2,col+2,row+2,col+2)
        
            ws.Cells(row+1,col+2).Value='Task'
            merge_and_align(ws,row+1,col+2,row+1,col+2)
        
            ws.Cells(row+3,col+2).Value='Task'
            merge_and_align(ws,row+3,col+2,row+3,col+2)
            
            for day in 今月2:
                #print("row=",row,"row_start=",row_start)
                col=col_start+day_col_offset+day_processed
                merge_and_align(ws,row,col,row,col+phases-1)
                merge_and_align(ws,row+2,col,row+2,col+phases-1)
                ws.Range(ws.Cells(row,col),ws.Cells(row+3,col+phases-1)).Borders.LineStyle = xlContinuous
                ws.Range(ws.Cells(row,col),ws.Cells(row+3,col+phases-1)).Borders.weight=xlThin
                day_processed +=phases
        else:
            data_start_col=col_start
            data_end_col=data_start_col+day_col_offset+phases*len(今月2)-1
            ws.Range(ws.Cells(row-4,data_start_col),ws.Cells(row-1,data_end_col)).Copy(ws.Range(ws.Cells(row,data_start_col),ws.Cells(row,data_start_col)))

    #Fill in Staff names
    list2 = [['' for i in range(1)] for j in range(len(All_Staff)*4)]#Constructor
    #wk=ws.Range(ws.Cells(row_start,col_start),ws.Cells(row_start+4*len(All_Staff)-1,col_start)).Value#Values ->2 dim array
    print("list2=",len(list2))
    #[[],[],[]...
    for person in All_Staff:
        list2[person*4][0]=staffdef[person]
    ws.Range(ws.Cells(row_start,col_start),ws.Cells(row_start+4*len(All_Staff)-1,col_start)).Value=list2#Restore it    
    ws.Rows(5).EntireRow.Hidden = True#Hide intemidiate Results

def get_relative_addr_str(ws,row,col):
    str=ws.Cells(row,col).address
    str=str.replace('$','')
    return str
def day_color_format2(ws,row,col,row_offset,one_month_mode=False):
    ws.Cells(row,col).FormatConditions.Delete()#Add(xlCellValue, xlGreaterEqual, '=85')

    if one_month_mode:
        ws.Cells(row,col).FormatConditions.Add(xlExpression, xlGreater,'=Month('+get_relative_addr_str(ws,row+row_offset,col)+')>$AZ$2')
        ws.Cells(row,col).FormatConditions.Add(xlExpression, xlEqual, '=Weekday('+get_relative_addr_str(ws,row+row_offset,col)+')=7') #SAT:7 Sun:1
        ws.Cells(row,col).FormatConditions.Add(xlExpression, xlEqual, '=Weekday('+get_relative_addr_str(ws,row+row_offset,col)+')=1') #SAT:7 Sun:1
        
        ws.Cells(row,col).FormatConditions(1).Interior.ColorIndex=2 #White
        ws.Cells(row,col).FormatConditions(2).Interior.ColorIndex=20 #LightBlue
        ws.Cells(row,col).FormatConditions(3).Interior.ColorIndex=22 #
    else:
        ws.Cells(row,col).FormatConditions.Add(xlExpression, xlEqual, '=Weekday('+get_relative_addr_str(ws,row+row_offset,col)+')=7') #SAT:7 Sun:1
        ws.Cells(row,col).FormatConditions.Add(xlExpression, xlEqual, '=Weekday('+get_relative_addr_str(ws,row+row_offset,col)+')=1') #SAT:7 Sun:1
        
        ws.Cells(row,col).FormatConditions(1).Interior.ColorIndex=20 #LightBlue
        ws.Cells(row,col).FormatConditions(2).Interior.ColorIndex=22 #
def merge_and_align(ws,row_start1,col_start1,row_end1,col_end1,weight=xlThin):
        

        print(row_start1,col_start1,row_end1,col_end1)

        ws.Range(ws.Cells(row_start1,col_start1), ws.Cells(row_end1,col_end1)).Merge()
        ws.Range(ws.Cells(row_start1,col_start1), ws.Cells(row_end1,col_end1)).VerticalAlignment = xlCenter
        ws.Range(ws.Cells(row_start1,col_start1), ws.Cells(row_end1,col_end1)).HorizontalAlignment = xlCenter
        ws.Range(ws.Cells(row_start1,col_start1), ws.Cells(row_end1,col_end1)).Borders.LineStyle = xlContinuous
        ws.Range(ws.Cells(row_start1,col_start1), ws.Cells(row_end1,col_end1)).Borders.Weight = weight

2022年6月20日月曜日

Pywin32によるExcel Format操作その2


 

フォーマットルーチンの高速化を行いました。データ部に関しては、2次元配列による高速化手法が適用可能ですが、フォーマットについては、その手は使えません。しかし、フォーマットのみなので、コピぺ手法が適用可能です。行っているのは、

1)Person0, Day0でフォーマットを作成、ThisMonthにコピペ展開

2)1)で作成したフォーマットをAll_Staffにコピペ展開

これで、35人28日の場合、10秒程度となりました。1セルづつ書き込むのに比べて、体感10倍速となりました。




def post_main():
    #pdb.set_trace()
    print("Hello post main world!")
    print('\n\n*********Processing Post operation *************\n')
    import win32com.client#Import pywin32
    phases=len(dayphase_list)
#
#Launch Excel
    try :
        xl = win32com.client.Dispatch("Excel.Application")
    except:
        print("can not invoke excel")
        exit()
    #Show working
    xl.Visible = False
    os.chdir(project_file_path)
    file=os.path.join(project_file_path,"excel_post_export.xlsx")
    
    wb = xl.Workbooks.Open(file)
    # Excelsheet object
    ws = wb.Worksheets(1)
    
    
    make_initial_format_improved(ws)
    #make_data_improved(ws,phases,True)
    
    wb.Close(True)# True:Save False=Default:Not Save
    # Excel Quit
    xl.Quit()
    print('\n\n*********Processed Post Operation.*************\n')

def clear(ws):
    ws.UsedRange.ClearContents()
    ws.UsedRange.ClearFormats()
    

def set_font(ws,addr,value):
    ws.Range(addr).Value=value
    ws.Range(addr).Font.Name = "Yu Gothic UI"
    # Set Cell font size
    ws.Range(addr).Font.Size = 14
    # Set Bold
    ws.Range(addr).Font.Bold = True


def make_initial_format_improved(ws):
    clear(ws)
    
    set_font(ws,"D2","Work Schedule")
    set_font(ws,"AW2","")
    #ws.Range("AW2").Formula='=Year(E7)'
    ws.Range("AW2","AY2").Merge()
    ws.Range("AW2").Font.Underline = xlUnderlineStyleSingle
    ws.Range("AW1","AY1").Merge()
    set_font(ws,"AW1",'Year')
    
    #ws.Range("AZ2").Formula='=Month(E7)'
    ws.Range("AZ2").Font.Underline = xlUnderlineStyleSingle
    ws.Range("AZ2","BB2").Merge()
    set_font(ws,"AZ1",'Month')
    ws.Range("AZ2","BB2").Merge()

    #ws.Range("BC2").Formula='=Month(E7)'
    ws.Range("BC2").Font.Underline = xlUnderlineStyleSingle
    ws.Range("BC2","BD2").Merge()
    set_font(ws,"BC1",'Day')
    ws.Range("BC1","BD1").Merge()
    
    row_start=8
    row=row_start
    col_start=2
    col=col_start
    day_col_offset=3
    persons=len(staffdef)
    days=len(ThisMonth)
    phases=len(dayphase_list)
    cols=phases*days
    day_processed=0
    row=row_start-2

    #Draw Top Header
    for day in ThisMonth:
        col=col_start+day_col_offset+day_processed
        if day_processed==0:
            day_str=daydef[day]
        
            #print(day_str,int(day_str[8:]))
            ws.Range("AW2").Formula='='+day_str[0:4]#Year
            ws.Range("AZ2").Formula='='+day_str[5:7]#Month
            ws.Range("BC2").Formula='='+day_str[8:]#Dat
               
            
            print(get_relative_addr_str(ws,row-1,col))
            ws.Cells(row-1,col).Formula='=Date(AW2,AZ2,BC2)'#
            ws.Cells(row  ,col).Formula='=Day('+get_relative_addr_str(ws,row-1,col)+')' 
            
            day_color_format2(ws,row,col,-1)
            merge_and_align(ws,row ,col,row,col+phases-1)
            
            ws.Cells(row+1  ,col).Formula='='+get_relative_addr_str(ws,row-1,col)   
            ws.Cells(row+1,col).NumberFormatLocal='aaa'
            day_color_format2(ws,row+1,col,-2)
            merge_and_align(ws,row+1,col,row+1,col+phases-1)
            
        elif day_processed==phases:
            ws.Cells(row-1,col).Formula='='+get_relative_addr_str(ws,row-1,col-phases)+'+1'
            ws.Cells(row  ,col).Formula='=Day('+get_relative_addr_str(ws,row-1,col)+')' 
            day_color_format2(ws,row,col,-1)
            merge_and_align(ws,row ,col,row,col+phases-1)
            
            ws.Cells(row+1  ,col).Formula='='+get_relative_addr_str(ws,row-1,col) #'=Day('+get_relative_addr_str(ws,row-1,col)+')'   
            ws.Cells(row+1,col).NumberFormatLocal='aaa'
            day_color_format2(ws,row+1,col,-2)
            merge_and_align(ws,row+1,col,row+1,col+phases-1)
            
            
        else:
            ws.Range(ws.Cells(row-1,col-phases),ws.Cells(row-1,col-1)).Copy(ws.Range(ws.Cells(row-1,col),ws.Cells(row-1,col)))
            ws.Range(ws.Cells(row,col-phases),ws.Cells(row+1,col-1)).Copy(ws.Range(ws.Cells(row,col),ws.Cells(row,col)))
        day_processed +=phases

    #Draw Person Property and Data Area
    row=row_start
    for person in All_Staff:
        day_processed=0
        row =row_start+person*4
        if person==All_Staff[0]:
            #name=staffdef[person]
            #ws.Cells(row,col_start).Value=name   
            col=col_start
            merge_and_align(ws,row,col,row+3,col)
            ws.Cells(row,col+1).Value='Preferred'
            merge_and_align(ws,row,col+1,row+1,col+1)

            ws.Cells(row+2,col+1).Value='Sc3 Solution'
            merge_and_align(ws,row+2,col+1,row+3,col+1)

            ws.Cells(row,col+2).Value='Shift'
            merge_and_align(ws,row,col+2,row,col+2)

            ws.Cells(row+2,col+2).Value='Shift'
            merge_and_align(ws,row+2,col+2,row+2,col+2)
        
            ws.Cells(row+1,col+2).Value='Task'
            merge_and_align(ws,row+1,col+2,row+1,col+2)
        
            ws.Cells(row+3,col+2).Value='Task'
            merge_and_align(ws,row+3,col+2,row+3,col+2)
            
            for day in ThisMonth:
                #print("row=",row,"row_start=",row_start)
                col=col_start+day_col_offset+day_processed
                merge_and_align(ws,row,col,row,col+phases-1)
                merge_and_align(ws,row+2,col,row+2,col+phases-1)
                ws.Range(ws.Cells(row,col),ws.Cells(row+3,col+phases-1)).Borders.LineStyle = xlContinuous
                ws.Range(ws.Cells(row,col),ws.Cells(row+3,col+phases-1)).Borders.weight=xlThin
                day_processed +=phases
        else:
            data_start_col=col_start
            data_end_col=data_start_col+day_col_offset+phases*len(ThisMonth)-1
            ws.Range(ws.Cells(row-4,data_start_col),ws.Cells(row-1,data_end_col)).Copy(ws.Range(ws.Cells(row,data_start_col),ws.Cells(row,data_start_col)))

    #Fill in Staff names
    list2 = [['' for i in range(1)] for j in range(len(All_Staff)*4)]#Constructor
    #wk=ws.Range(ws.Cells(row_start,col_start),ws.Cells(row_start+4*len(All_Staff)-1,col_start)).Value#Values ->2 dim array
    print("list2=",len(list2))
    #[[],[],[]...
    for person in All_Staff:
        list2[person*4][0]=staffdef[person]
    ws.Range(ws.Cells(row_start,col_start),ws.Cells(row_start+4*len(All_Staff)-1,col_start)).Value=list2#Restore it    
    ws.Rows(5).EntireRow.Hidden = True#Hide intemidiate Results

def get_relative_addr_str(ws,row,col):
    str=ws.Cells(row,col).address
    str=str.replace('$','')
    return str
def day_color_format2(ws,row,col,row_offset):
    ws.Cells(row,col).FormatConditions.Delete()#Add(xlCellValue, xlGreaterEqual, '=85')

    ws.Cells(row,col).FormatConditions.Add(xlExpression, xlEqual, '=Weekday('+get_relative_addr_str(ws,row+row_offset,col)+')=7') #SAT:7 Sun:1
    ws.Cells(row,col).FormatConditions.Add(xlExpression, xlEqual, '=Weekday('+get_relative_addr_str(ws,row+row_offset,col)+')=1') #SAT:7 Sun:1
        
    ws.Cells(row,col).FormatConditions(1).Interior.ColorIndex=20 #LightBlue
    ws.Cells(row,col).FormatConditions(2).Interior.ColorIndex=22 #
def merge_and_align(ws,row_start1,col_start1,row_end1,col_end1,weight=xlThin):
        

        print(row_start1,col_start1,row_end1,col_end1)

        ws.Range(ws.Cells(row_start1,col_start1), ws.Cells(row_end1,col_end1)).Merge()
        ws.Range(ws.Cells(row_start1,col_start1), ws.Cells(row_end1,col_end1)).VerticalAlignment = xlCenter
        ws.Range(ws.Cells(row_start1,col_start1), ws.Cells(row_end1,col_end1)).HorizontalAlignment = xlCenter
        ws.Range(ws.Cells(row_start1,col_start1), ws.Cells(row_end1,col_end1)).Borders.LineStyle = xlContinuous
        ws.Range(ws.Cells(row_start1,col_start1), ws.Cells(row_end1,col_end1)).Borders.Weight = weight

2022年6月19日日曜日

Pywin32によるExcel Format操作その1

 Excelに書き込めるようにはなりましたが、フォーマットもPythonで書き込みたいということで試行錯誤しながらトライ中です。

下図は、Pywin32でフォーマットしたExcelですが、書くのに2分位かかっていました。フォーマット自体は、そのインスタンスについて1回しか使用しないので、遅くてもよいのですが、データ書き込みも1分以上、かかってしまっており、改善の必要があります。

Pywin32を用いたExcel操作は、あまり例が見当たらないと思いますので、使ったソースはそのまま公開します。ScheduleNurse3に依存する処理はないので、操作例として利用可能と思います。

以下は、現状でのフォーマットルーチンです。
難しいのは、土日に色を付けるところで、FormatConditionで記述しています。(恐らくは、仕様追加.追加..の産物なのでしょうが、VBAの言語仕様にたまげてしまいました。)
大抵は、VBAをそのままコンバート出来ますが、オブジェクト変数に落とす部分は、Pythonでは記述不能です。Unionで動的にオブジェクトを追加していくことは出来ないのは残念です。なので、VBAの究極の記述と比較すると、速度低下は避けられないと思います。

def make_initial_format(ws):

    
    print(locale.getdefaultlocale())
    dt = datetime.datetime(2018, 1, 1)
    print(dt)
# 2018-01-01 00:00:00

    print(dt.strftime('%A, %a, %B, %b'))
# Monday, Mon, January, Jan
    set_font(ws,"D2","Work Schedule")
    set_font(ws,"AW2","")
    ws.Range("AW2").Formula='=Year(E7)'
    ws.Range("AW2","AX2").Merge()
    ws.Range("AW2").Font.Underline = xlUnderlineStyleSingle
    ws.Range("AW1","AX1").Merge()
    set_font(ws,"AW1",'Year')
    set_font(ws,"AZ2","2022")#dummy
    ws.Range("AZ2").Formula='=Month(E7)'
    ws.Range("AZ2").Font.Underline = xlUnderlineStyleSingle
    ws.Range("AZ2","BB2").Merge()
    set_font(ws,"AZ1",'Month')
    ws.Range("AZ1","BB1").Merge()

    row_start=8
    row=row_start
    col_start=2
    col=col_start
    day_col_offset=3
    persons=len(staffdef)
    days=len(ThisMonth)
    phases=len(dayphase_list)
    cols=phases*days
#Clear All
    ws.Range(ws.Cells(row_start,col_start+day_col_offset), ws.Cells(row_start+4*persons-1,col_start+day_col_offset+cols-1)).Value=""
    ws.Range(ws.Cells(row_start,col_start+day_col_offset), ws.Cells(row_start+4*persons-1,col_start+day_col_offset+cols-1)).Interior.Color=0xffffff

    ws.Columns(2).EntireColumn.ColumnWidth = 30
    ws.Columns(3).EntireColumn.ColumnWidth = 10
    ws.Columns(4).EntireColumn.ColumnWidth = 8

    for name in staffdef:
        ws.Cells(row,col_start).Value=name   
        merge_and_align(ws,row,col,row+3,col)
        ws.Cells(row,col+1).Value='Preferred'
        merge_and_align(ws,row,col+1,row+1,col+1)

        ws.Cells(row+2,col+1).Value='Sc3 Solution'
        merge_and_align(ws,row+2,col+1,row+3,col+1)

        ws.Cells(row,col+2).Value='Shift'
        merge_and_align(ws,row,col+2,row,col+2)

        ws.Cells(row+2,col+2).Value='Shift'
        merge_and_align(ws,row+2,col+2,row+2,col+2)
        
        ws.Cells(row+1,col+2).Value='Task'
        merge_and_align(ws,row+1,col+2,row+1,col+2)
        
        ws.Cells(row+3,col+2).Value='Task'
        merge_and_align(ws,row+3,col+2,row+3,col+2)
        row +=4
    day_processed=0
    
    row=row_start-2
    #import pdb
    #pdb.set_trace()
    #print("set trace")
    
    pdb.set_trace()
    for day in ThisMonth:
        day_str=daydef[day]
        
        print(day_str,int(day_str[8:]))
        col=col_start+day_col_offset+day_processed
        for ph in range(phases):
            ws.Columns(col+ph).EntireColumn.ColumnWidth = 2

        #ws.Cells(row,col).Value=int(day_str[8:])
        ws.Cells(row,col).Formula='=Day(Date('+day_str[0:4]+','+day_str[5:7]+','+day_str[8:]+'))'
        day_color_format(ws,row,col,day_str)
        merge_and_align(ws,row ,col,row,col+phases-1)

        ws.Cells(row+1,col).Formula='=Date('+day_str[0:4]+','+day_str[5:7]+','+day_str[8:]+')'
        ws.Cells(row+1,col).NumberFormatLocal='aaa'
        day_color_format(ws,row+1,col,day_str)

        merge_and_align(ws,row+1,col,row+1,col+phases-1)

        day_processed +=phases

    row=row_start
    for person in All_Staff:
        day_processed=0
        row =row_start+person*4
        for day in ThisMonth:
            #print("row=",row,"row_start=",row_start)
            col=col_start+day_col_offset+day_processed
            merge_and_align(ws,row,col,row,col+phases-1)
            merge_and_align(ws,row+2,col,row+2,col+phases-1)

            day_processed +=phases
        ws.Range(ws.Cells(row,col_start), ws.Cells(row+3,col_start+day_col_offset+len(ThisMonth)*phases-1)).Borders.LineStyle = xlContinuous
        ws.Range(ws.Cells(row,col_start), ws.Cells(row+3,col_start+day_col_offset+len(ThisMonth)*phases-1)).Borders.weight=xlThin

#Sub rountines
def get_weekday(day_str):
    date=datetime.day(int(day_str[0:4]),int(day_str[5:7]),int(day_str[8:]))
    return date.weekday()#return integer Monday=0

def day_color_format(ws,row,col,day_str):
    ws.Cells(row,col).FormatConditions.Delete()#Add(xlCellValue, xlGreaterEqual, '=85')

    ws.Cells(row,col).FormatConditions.Add(xlExpression, xlEqual, '=Weekday(Date('+day_str[0:4]+','+day_str[5:7]+','+day_str[8:]+'))=7') #SAT:7 Sun:1
    ws.Cells(row,col).FormatConditions.Add(xlExpression, xlEqual, '=Weekday(Date('+day_str[0:4]+','+day_str[5:7]+','+day_str[8:]+'))=1') #SAT:7 Sun:1
        
    ws.Cells(row,col).FormatConditions(1).Interior.ColorIndex=20 #LightBlue
    ws.Cells(row,col).FormatConditions(2).Interior.ColorIndex=22 #

def merge_and_align(ws,row_start1,col_start1,row_end1,col_end1,weight=xlThin):
        
#        if row_start<=0:
        print(row_start1,col_start1,row_end1,col_end1)
#            pdb.set_trace()
        ws.Range(ws.Cells(row_start1,col_start1), ws.Cells(row_end1,col_end1)).Merge()
        ws.Range(ws.Cells(row_start1,col_start1), ws.Cells(row_end1,col_end1)).VerticalAlignment = xlCenter
        ws.Range(ws.Cells(row_start1,col_start1), ws.Cells(row_end1,col_end1)).HorizontalAlignment = xlCenter
        ws.Range(ws.Cells(row_start1,col_start1), ws.Cells(row_end1,col_end1)).Borders.LineStyle = xlContinuous
        ws.Range(ws.Cells(row_start1,col_start1), ws.Cells(row_end1,col_end1)).Borders.Weight = weight

グループ集合の下の画面に 出てくる名前の群は何を表すのでしょうか

Ans. 定義した集合の確認用です。

ユーザマニュアルグループ集合をご参照ください。



2022年6月17日金曜日

デバッガの起動2

今回は、制約生成における例です。

メインに定型の2行を設置します。

求解で、set_trace()の次の行で停止します。タブは、制約生成のデバッグは、ソース全体タブのページになります。



ブレークポイントの設置は、緑の実行、ステップボタンがあるときだけ、可能です。
set_trace()実行前に設置することは出来ません。
ツールチップしなが動作を確認します。

大きなオブジェクトは、ツールチップに入りきらないので、求解右側ペインでご確認ください。
Pythonでの制約生成が終了すると、生成した制約に基づいての最適化が始まります。
このとき、実行、ステップボタンは消失しています。


 最適化が終了すると、今度は、ポスト全体での処理に移ります。set_trace()が設定されていれば、そこでストップします。

つまり、
1)Pythonで制約化 -デバッガ付属
2)最適化実行
3)出力解を用いたPythonによるポスト処理(Excel等)-デバッガ付属

という、一連の流れを一つのプラットフォーム上で処理することができます。

2022年6月16日木曜日

デバッガの起動

 デバッガを起動するには、デバッグしたい箇所付近に次の2文を打ち込んでください。

import pdb

pdb.set_trace()

set_traceを実行後の行で停止します。

下の画面では、set_trace行の次の行で黄色(停止)しているのが分かります。

ScheduleNurseⅢのPythonは、本来の目的である制約の作成のほか、解を求めた後の整形処理用(ポスト)の二つがあります。post_mainで記述された部分が、解整形用のスクリプトになります。今回は、Excelシートに所定フォーマットで解を書き込む処理を行っています。


<ステップを抜ける>

ステップボタンをクリックするとステップ動作しますが、停滞する場合もあります。

求解ページの右画面に実行の行情報が出力されていますが、外部ライブラリを実行している場合が殆どだと思います。外部ライブラリでは、コンパイルされたDLLが実行されており、Pythonソースは添付されていません。よって外部ライブラリのステップ動作をエディタ上で見ることは出来ません。

ですので、下のように、行番号部分をクリックしてブレークポイントを設定し、実行ボタンで、ブレークポイントまで実行させて、ライブラリ内部から抜けてください。


<ブレークポイントのオン・オフ>

もう一度クリックするとオフになります。コメント行にブレークポイントを設定することは出来ません。

次の画面では、Excelが起動したところまで実行しました。(Excelがないとエラーが出て終了してしまいます。)


読み込み用のExcelシートがないと次のように、エラーになってしまいます。
をダブルクリックするとソース行に飛びます。ポスト全体(ReadOnly)ではなく、ソース行に飛ぶようにしました。
ExceFormatは、別に作ってプロジェクトと同じフォルダに置きます。
さらに実行させます。
今度は、reがないと言ってきました。reは、正規表現モジュールです。
記述忘れですので追加します。

追加部は、最初に追加します。(post_mainでは動きませんでした。)


さらに、実行します。またエラーが出てしまいました。
調べたところ、Excelでスタッフ名を検索していますが、見つからないとNonTypeを返してくるようでした。




部長(兼任)の括弧が微妙に違っていたようです。(このようなバグは、良く起きます。)また、名前が変更になっていた部分もありました。スケジュールナースⅢのスタッフ名から、Excelにコピーするようにすれば、このようなミスは防げると思います。
Excelは、1個1個COMを呼ぶので、遅いです。動いているかどうか心配になるので、上のように処理中のスタッフ名を記述しました。

このようにして、ようやくExcelに出力することが出来ました。

以上、Excel整形もスケジュールナースⅢ内蔵Pythonで完結することが出来ました。






2022年6月15日水曜日

デバッガによるデバッグ

 



デバッガのデバッグ中です。日本語変数もツールチップ出来ます。


未だ、代入されていない変数は、次のようにエラーが表示されます。黄色部は、停止行で、未だ実行されていません。怪しい箇所付近にブレークポイントを置いて、中断、付近の変数を調べます。
Pythonの入門としても使えそうな雰囲気です。

元々、PythonからExcelに直接、解を整理した形で出力しようと思っていました。当初は、組み込み関数print()でオブジェクトを出力して終了!、と思っていたのですが、どうもそれだけでは、「未だ大変すぎる」、と思いデバッガ実装を行うことにしました。

他の処理系IDEに任せることも検討しましたが、完成後は、結局スケジュールナースⅢでその都度、走らせることになるので、開発/メンテナンス等の移行手間を考えると、手間が増える、という結論に達しビルトインが理想、という結論になりました。

本当に必要なのは、制約が正しく行われているか?ということなのですが、それについては、このようなデバッガを用意したとしても、無力に近いです。制約そのものと、Pythonとは全く関係ありません。
しかし、実行を追えば、正しく制約文に到達しているかどうか、位は分かります。Pythonの変数オブジェクトは、独特で、多くの情報を含んでいますが、ツールチップによる現在値表示は、その点、大変役にたつと思います。いちいちprint()を置く必要はなくなります。

このようなPythonを用いて制約しつつ、なおかつGUIで、その実行途中の様子を見ることが出来る処理系は世の中には無く、ScheduleNurseⅢの世界初の成果ではないかと思います。古くは、SystemVerilogシミュレータを構築しようとしたときから、その発想はありましたが、時代が進んだおかげで、短期間に実装することが出来そうです。エディタにしろ、言語Pythonにしろ、一から設計・実装するのは現実的ではありません。多くの過去の資産を生かしつつ新しいものを作り出すことは、とても楽しい作業です。

ミソは、CUIとGUIの融合です。両者を組み合わせることにより、非常に簡便に、きめ細かい制約を、少ない記述で行うことが出来ました。

また、別のプログラムとして、解の整理を行ってユーザフォーマット上のExcelに直接打ち出すことが出来るようになります。従来VBAで行っていた処理もスケジュールナース内で完結出来るようになるでしょう。


2022年6月14日火曜日

Python 3.6から3.10へ

 次のバージョンリリースより内蔵PythonのVersionを3.10に変更します。

変更理由

デバッガを内蔵する関係で、Pythonのインタープリタをstringベースからfileベースに変更したところ、次のようなUTF-8関係のパースエラーが出るためです。

SyntaxError: Non-UTF-8 code starting with '\xe4' in file test6.py on line 201, but no encoding declared; see http://python.org/dev/peps/pep-0263/ for details

次のバージョンでは、Pdbに対応します。ステップ実行、ブレークポイント設定、

変数のツールチップ表示等は、GUIでも対応しています。通常のデバッグでは、GUIだけの使用で十分だと思います。(コマンドも打ち込むことは出来ます。)

Pdbは、pythonに付属するデバッガです。

2022年6月13日月曜日

Pywin32によるExcel整形その2

 別なプロジェクトでも記述してみました。訪問クリニックプロジェクトです。従来のCSVは、次のような出力をしていました。

これをEXCEL整形手法で同じようにExcelに直接出力します。ついでに予定も付加しています。
同じ内容ですが、断然、見易さが違います。予定との齟齬チェックも容易です。


コードは以下のように結構な量です。
前のコードもそうですが、100行を超えるとデバッガなしではつらいものがあります。
なので、デバッガを実装することにします。
def post_main():
    
    xl,wb,ws=invoke_excel()
    for person in 全スタッフ:
        staff_name=staffdef[person]
        c=ws.Range("B1:B51").Find(staff_name)
        #print(c.Row,c.Column)
        for day in 今月:
            col=day-制約開始日
            #print(day)
            
            shift_label=shift_schedules[person][day][2]
            ws.Cells(c.Row,c.Column+3+col).Value=shift_label#シフト予定
            
            shift_color=get_shift_label_color(shift_label)
            ws.Cells(c.Row,c.Column+3+col).Interior.Color=shift_color
            draw_cell(ws,c.Row+1,c.Column+3+col,person,day)
    wb.Close(True)# Trueで保存。False=Defaultでブックを保存せずにクローズ
    # Excel終了
    xl.Quit()
    print('\n\n*********ポスト処理を実行終了しました。*************\n')


def draw_cell(ws,row,col,person,day):
    phases=3
    print(person,day)
    t0=task_solution[person][day*phases+0]
    t1=task_solution[person][day*phases+1]
    t2=task_solution[person][day*phases+2]


#勤務処理
    if t0=='日T' and t1=='日T':
        if day in 休勤日今月:
            label='休勤'
            color='128,128,100'
        else :
            label='日勤'
            color='255,128,128'
    elif t0=='日T' and t1=='有給':
        label='後有'
        color='128,255,128'
    elif t1=='日T' and t0=='有給':
        label='前有'
        color='128,128,255'
    elif t0=='有給' and t1=='有給':
        label='有休'
        color='200,200,200'
    elif t0=='公休' and t1=='公休':
        label='休み'
        color='200,255,200'
    elif t0=='日T' and t1=='公休':
        label='午前'
        color='255,200,200'
    elif t1=='日T' and t0=='公休':
        label='午後'
        color='200,200,255'
    elif t0=='希望休み' and t1=='希望休み':
        label='希休'
        color='200,128,200'
    else:
        label='M休'
        color='128,200,128'
    ws.Cells(row,col).Value=label
    ws.Cells(row,col).Interior.Color=rgb_str_to_hex(color)

#属性処理
    label=''
    color='255,255,255'
    if t2 !='公休':
        if day in 土:
            label='土拘'
            color='200,128,200'
        else :
            label='拘束'
            color='128,128,255'
        if shift_schedules[person][day]=='希望休み扱い':
            if row0[d] !='希休':
                label='希休'
                color='128,128,255'
    ws.Cells(row+1,col).Value=label
    ws.Cells(row+1,col).Interior.Color=rgb_str_to_hex(color)

#すでにExcelが起動されている場合はそのタスクが使われる
#エラー終了するとタスクは残ります
def invoke_excel():
    sc3.hook_stdout()#printをリダイレクトする
    print('\n\n*********ポスト処理を実行中です。*************\n')
    import win32com.client#pywin32をインポート

    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,"excel_post_export.xlsx")
    file.replace("/","\\") #なぜか逆スラッシュでないと動かない
    wb = xl.Workbooks.Open(file)
    # Excelシートオブジェクト
    ws = wb.Worksheets(1)

    # 指定したシートを選択
    # Select()の使用前にシートのActivate()が必要
    ws.Activate()
    return xl,wb,ws

def get_shift_label_color(shift_label):
    if len(shift_label)==0 or shift_label=='・':
        return 0xffffff #空白ならWhiteを返す
    for item_array in shift_label_color_map.values():#task集合から探して
        for item in item_array:#taskは複数のラベルを持つ
            shift_label_candidate=item[0]
            #print(shift_label,shift_label_candidate)
            if shift_label==shift_label_candidate:#ラベル名が一致したなら
                color_str=item[1]
                rgb = re.findall(r"\d+",color_str)
                if len(rgb)==3:
                    return rgb_to_hex(rgb)
                else:
                    return 0xffffff #rgb形式でなければWhiteを返す
    for item in shift_collections_def.values():#無かったらtask aggregatesから探す
        shift_label_candidate=item[0]
        #if len(task_label)>=1:
        #    print("list",task_label,task_label_candidate)
        if shift_label==shift_label_candidate:
            #print("matched",task_label_candidate)
            color_str=item[1]
            rgb = re.findall(r"\d+",color_str)
            if len(rgb)==3:
                return rgb_to_hex(rgb)
            else:
                return 0xffffff #rgb形式でなければWhiteを返す
    for item in phase_objects_def.values():
        shift_label_candidate=item[0]
        if shift_label==shift_label_candidate:
            color_str=item[1]
            rgb = re.findall(r"\d+",color_str)
            if len(rgb)==3:
                return rgb_to_hex(rgb)
            else:
                return 0xffffff #rgb形式でなければWhiteを返す
    for item in phase_aggregate_object_def.values():
        shift_label_candidate=item[0]
        if shift_label==shift_label_candidate:
            color_str=item[1]
            rgb = re.findall(r"\d+",color_str)
            if len(rgb)==3:
                return rgb_to_hex(rgb)
            else:
                return 0xffffff 
    #print("unmatched",task_label)
    return 0xffffff #なければWhiteを返す  

def get_task_label(task):
    
    if task in  task_label_color_map:
        return task_label_color_map[task][0][0]
    else:
        print("Invalid Task", task)
        exit()
def rgb_to_hex(rgb):
    '''
    ws.Cells(1, i).Interior.color uses bgr in hex

    '''
    bgr = (int(rgb[2]),int(rgb[1]),int(rgb[0]))
    strValue = '%02x%02x%02x' % bgr
    # print(strValue)
    iValue = int(strValue, 16)
    return iValue
def rgb_str_to_hex(color_str):
    rgb = re.findall(r"\d+",color_str)
    if len(rgb)==3:
        return rgb_to_hex(rgb)
    else:
        return 0xffffff

2022年6月12日日曜日

Pywin32によるExcel整形出力

 ソルバのPythonソース出力追加を行いました。デバッグ用としてお医者さまプロジェクトを使用しました。このプロジェクトは、シフト解とタスク解、両方とも有意な値を持ち、なおかつ、シフト・タスク予定が詰まっていて検証用として最適です。(Covid-19支援プロジェクト)

最初は、従来のCSV出力例です。シフト・タスク出力を唯一つのファイルに出力したものですが、日付等もないので、もう一段整形が必要であること分かります。

次は、今回の出力です。
予定ラベルを追加した他、出力をラベル出力として、色を定義のものに合わせています。なおかつ、見易くするために夜勤に纏わる部分だけの出力に整形しています。
そして、お客さまフォーマットに合わせて、解を出力しています。例えば、スタッフの順序を入れ替れたり、空白行があっても、問題なく出力されます。つまりそのまま最終出力としても良い形に(ご自分で)整形が可能になった、ということです。

以下は、今回のpythonソースです。予定シフト、予定タスク、解シフト、解タスクをラベルで出力しています。

<Scan Objects>
面倒なのは、ラベル色の処理で、該当するラベルは、以下を検索して一致するものを出力する必要があります。

予定タスク
    task→task_aggregate

予定シフト

    shift→shift_aggregate→phase_variables→phase_variable_aggregate

をScanする必要があります。

<Alias Labelの処理>

また、予定シフトがAliasで記述されていた場合、例えば、今回”業務”は、”業務AM”、"業務PM”をAliasとして持ちます。予定がそういう風に記述されていたら、解ラベルもそれに応じて、同じラベルで出力するのが親切でしょう。

def post_main():
    sc3.hook_stdout()#printをリダイレクトする
    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,"excel_post_export.xlsx")
    file.replace("/","\\") #なぜか逆スラッシュでないと動かない
    wb = xl.Workbooks.Open(file)
    # Excelシートオブジェクト
    ws = wb.Worksheets(1)

    # 指定したシートを選択
    # Select()の使用前にシートのActivate()が必要
    ws.Activate()
    #print(task_label_color_map['日直'][0])
    #return
    for person in 全スタッフ:
        staff_name=staffdef[person]
        c=ws.Range("B1:B51").Find(staff_name)
        #print(c.Row,c.Column)
        for day in 今月:
            col=day-制約開始日
            #print(day)
            tph=col*2
            shift_label=shift_schedules[person][day][2]
            ws.Cells(c.Row,c.Column+3+col*2).Value=shift_label#シフト予定
            shift=shift_schedules[person][day][0]
            shift_color=get_shift_label_color(shift_label)    
            ws.Cells(c.Row,c.Column+3+col*2).Interior.Color=shift_color#シフト予定色


            t02=task_schedules[person][day*2][2]#label
            t0_color=get_task_label_color(t02)
            t00=task_schedules[person][day*2][0]
            
            t12=task_schedules[person][day*2+1][2]#label
            t1_color=get_task_label_color(t12)
            t10=task_schedules[person][day*2+1][0]
            
            ws.Cells(c.Row+1,c.Column+3+tph).Value=t02#タスク予定
            ws.Cells(c.Row+1,c.Column+3+tph).Interior.Color=t0_color
            ws.Cells(c.Row+1,c.COlumn+3+tph+1).Value=t12#タスク予定
            ws.Cells(c.Row+1,c.Column+3+tph+1).Interior.Color=t1_color

            shift=shift_solution[person][day]
            if shift=='業務なし日勤':
                ws.Cells(c.Row+2,c.Column+3+col*2).Value='' #empty
                ws.Cells(c.Row+2,c.Column+3+col*2).Interior.Color=0xffffff
            else:
                label_color=get_shift_label(shift,shift_label)#return (label,color_str)
                ws.Cells(c.Row+2,c.Column+3+col*2).Value=label_color[0]
                ws.Cells(c.Row+2,c.Column+3+col*2).Interior.Color=rgb_str_to_hex(label_color[1])

            task0=task_solution[person][day*2]
            task1=task_solution[person][day*2+1]
            t0_color=get_task_color(task0)
            t1_color=get_task_color(task1)
            ws.Cells(c.Row+3,c.Column+3+tph).Interior.Color=t0_color
            ws.Cells(c.Row+3,c.Column+3+tph+1).Interior.Color=t1_color

            if task0=='' or task0=='DummyDay':
                ws.Cells(c.Row+3,c.Column+3+tph).Value=''
            else:
                task_label=get_task_label(task0)
                ws.Cells(c.Row+3,c.Column+3+tph).Value=task_label
            if task1=='' or task1=='Dummy':
                ws.Cells(c.Row+3,c.Column+3+tph+1).Value=''
            else:
                task_label=get_task_label(task1)
                ws.Cells(c.Row+3,c.Column+3+tph+1).Value=task_label


    wb.Close(True)# Trueで保存。False=Defaultでブックを保存せずにクローズ
    # Excel終了
    xl.Quit()
    print('\n\n*********ポスト処理を実行終了しました。*************\n')

def get_shift_label(shift,scheduled_shift_label):
    for key in shift_label_color_map.keys():
        if key ==shift:
            item_array=shift_label_color_map[key]
            i=0
            for item in item_array:
                if i==0:
                    solution_shift_label=item[0]
                    solution_label_color=item[1]
                else:
                    if len(scheduled_shift_label)>=1 and item[0]==scheduled_shift_label:
                        return item[0],item[1]
                i+=1
            return solution_shift_label,solution_label_color
    print("Fatal Error",shift,scheduled_shift_label)
    exit()

def get_task_label(task):
    
    if task in  task_label_color_map:
        return task_label_color_map[task][0][0]
    else:
        print("Invalid Task", task)
        exit()
def rgb_to_hex(rgb):
    '''
    ws.Cells(1, i).Interior.color uses bgr in hex

    '''
    bgr = (int(rgb[2]),int(rgb[1]),int(rgb[0]))
    strValue = '%02x%02x%02x' % bgr
    # print(strValue)
    iValue = int(strValue, 16)
    return iValue

def get_task_label_color(task_label):
    if len(task_label)==0 or task_label=='.':
        return 0xffffff #空白,Dummy DummyDay
    for item_array in task_label_color_map.values():#task集合から探して
        for item in item_array:#taskは複数のラベルを持つ
            task_label_candidate=item[0]
            if task_label==task_label_candidate:#ラベル名が一致したなら
                color_str=item[1]
                rgb = re.findall(r"\d+",color_str)
                if len(rgb)==3:
                    return rgb_to_hex(rgb)
                else:
                    return 0xffffff #rgb形式でなければWhiteを返す
    for item in task_collections_def.values():#無かったらtask aggregatesから探す
        task_label_candidate=item[0]
        #if len(task_label)>=1:
        #    print("list",task_label,task_label_candidate)
        if task_label==task_label_candidate:
            #print("matched",task_label_candidate)
            color_str=item[1]
            rgb = re.findall(r"\d+",color_str)
            if len(rgb)==3:
                return rgb_to_hex(rgb)
            else:
                return 0xffffff #rgb形式でなければWhiteを返す
    #print("unmatched",task_label)
    return 0xffffff #なければWhiteを返す

def get_shift_label_color(shift_label):
    if len(shift_label)==0 or shift_label=='・':
        return 0xffffff #空白ならWhiteを返す
    for item_array in shift_label_color_map.values():#task集合から探して
        for item in item_array:#taskは複数のラベルを持つ
            shift_label_candidate=item[0]
            #print(shift_label,shift_label_candidate)
            if shift_label==shift_label_candidate:#ラベル名が一致したなら
                color_str=item[1]
                rgb = re.findall(r"\d+",color_str)
                if len(rgb)==3:
                    return rgb_to_hex(rgb)
                else:
                    return 0xffffff #rgb形式でなければWhiteを返す
    for item in shift_collections_def.values():#無かったらtask aggregatesから探す
        shift_label_candidate=item[0]
        #if len(task_label)>=1:
        #    print("list",task_label,task_label_candidate)
        if shift_label==shift_label_candidate:
            #print("matched",task_label_candidate)
            color_str=item[1]
            rgb = re.findall(r"\d+",color_str)
            if len(rgb)==3:
                return rgb_to_hex(rgb)
            else:
                return 0xffffff #rgb形式でなければWhiteを返す
    for item in phase_objects_def.values():
        shift_label_candidate=item[0]
        if shift_label==shift_label_candidate:
            color_str=item[1]
            rgb = re.findall(r"\d+",color_str)
            if len(rgb)==3:
                return rgb_to_hex(rgb)
            else:
                return 0xffffff #rgb形式でなければWhiteを返す
    for item in phase_aggregate_object_def.values():
        shift_label_candidate=item[0]
        if shift_label==shift_label_candidate:
            color_str=item[1]
            rgb = re.findall(r"\d+",color_str)
            if len(rgb)==3:
                return rgb_to_hex(rgb)
            else:
                return 0xffffff 
    #print("unmatched",task_label)
    return 0xffffff #なければWhiteを返す  
def get_task_color(task):
    if len(task)==0 or task=='Dummy' or task=='DummyDay':
        return 0xffffff
    if task in  task_label_color_map:
        color_str=task_label_color_map[task][0][1]
        rgb = re.findall(r"\d+",color_str)
        if len(rgb)==3:
            return rgb_to_hex(rgb)
        else:
            return 255 + 255*256 + 255*256*256
    else:
        return 255 + 255*256 + 255*256*256


def rgb_str_to_hex(color_str):
    rgb = re.findall(r"\d+",color_str)
    if len(rgb)==3:
        return rgb_to_hex(rgb)
    else:
        return 0xffffff