2012年8月17日 星期五

a problem Read & ReadEx in Powerbuilder 10

Powerbuilder 10 (含)以後的版本,檔案的讀取分為 Read 與 ReadEx 兩個指令,按照說明文件的用法 ReadEx 支援 StreamMode! 讀取每次可以超過 32767 個位元組,並且支援 TextMode! 的讀取模式,算是很好用的;但是這次的問題卻出在使用 LineMode! 開啟檔案 時 PB10 的 Read 這個指令上:

先看看這段程式碼哪裡出了問題,我有純文字一份文件 A.txt (ANSI編碼)內容為:
數據窗口揭秘:未公開的數據窗口事件 (作者:Mark Brown) 到目前為止,PB的數據窗口控件仍是PB眾多控件中功能最強大,最複雜的控件。 數據窗口固有的行為
然後利用下面這段程式碼讀取內容並顯示在 mle_1(MultiLine Editor)上面
integer li_fn string ls_str mle_1.text = "" li_fn = FileOpen("A.txt",LineMode!,Read!) if li_fn > 0 then //逐行讀取並顯示在mle_1上面 do while FileRead(li_fn , ls_str) >= 0 mle_1.text += ls_str +"~r~n" loop FileClose(li_fn) end if
mle_1物件上的顯示內容:
數據窗口揭秘:未公開的數據窗口事件 的數據窗口事件 事件 (作者:Mark Brown) ) 到目前為止,PB的數據窗口控件仍是PB眾多控件中功能最強大,最複雜的控件。 件中功能最強大,最複雜的控件。 複雜的控件。 鞳C 數據窗口固有的行為 的行為
發現了嗎?讀取出的內容怪怪的,幾乎每一行後段文字都重複被讀出,導致整個內容大亂。
但是,這段程式碼如果放在 PB6、PB7、PB8、PB9 執行,卻一點問題就沒有。

整個詭異到不行。

但是如果是英數文內容的檔案,不管哪個版本 PB6 or PB10 就完全不會有問題,測試檔案內容如下:

calculate the height of the element and divides that by two to center it the value is inversed so as to pull the window above the target the offset of 12 is added to account for the height of the field form element
後來,我把 Read 改成 ReadEx 指令後,在進行一次測試,發現無此問題:

integer li_fn string ls_str mle_1.text = "" li_fn = FileOpen("A.txt",LineMode!,Read!) if li_fn > 0 then //逐行讀取並顯示在mle_1上面 do while FileReadEx(li_fn , ls_str) >= 0 mle_1.text += ls_str +"~r~n" loop FileClose(li_fn) end if
看起來像是中文字造成衝碼問題,但仔細檢查被中斷文字前後都沒有所謂衝碼字元,以前PB6會有蚯蚓符號『~』的衝碼問題,PB10以後應該不會有此問題啊?

後來我注意到,讀出的內容有種巡迴式的重覆中斷現象,不像單純字碼衝碼的現象,且 ReadEx 指令是PB10以後的版本第一次出現的新指令,而 PB10 的特性是支援 Unicode ,所以就假設會不會是字元長度計算錯誤,由其比較明顯的是在 ANSI 編碼文字下,中文是 2Byte (半形)英數字僅 1Byte。
所以就用此假設試算一下 A.txt 的第一行:
數據窗口揭秘:未公開的數據窗口事件
此段文字不包含換換行字元共 34Byte 採用 ANSI 計算長度就是 34 ,
若採 unicode 算法(一個中文只會當成一個字)其實只得到長度 17,
假設 PB10 底層計算採用了 unicode 方式計算了該字串的長度 17 加上換行 2 個字元,
共 19 個字元
由於檔案是ANSI編碼,因此當 PB10 進行 File Seek 定位時會以 Byte 為單位計算到下一行起始字元的動作時,就會定位到該行『的』這個字,所以下一次 FileRead 時就會變成讀到『的數據窗口事件』這段文字。

得到以上結論以後,我更大膽架設,如果 A.txt 是 Unicode 編碼的話就不會有此現象,所以將 A.txt 用記事本開啟以後,另存新檔,並將編碼方式改成 Unicode ,重新以第一段程式碼讀取。

果然就不會出現此現象了。

為什麼純英數文字檔案不會有此問題? 那是因為純英數文字在ANSI編碼下一個字就是長度 1,而 Unicode 下也是一個字長度也是 1,這就是為什麼不會錯的問題。

尤其 PB10 以後的版本是以 unicode 為主體,所以長度計算指令 Lan(stringblob)算出來的是 unicode 長度,若要計算 ANSI 長度就要使用 LanA(Stringblob) 這個指令。

所以,在  PB10 的 Help 中關於 FileOpen 的說明有一段耐人尋味的描述:
FileRead and FileWrite have limitations on the amount of data that can be read or written and are maintained for backward compatibility.
也就是說為了保持相容性,對這些舊指令加了一些限制,也畢竟是外國人設計的軟體,通常開發者對其他雙字元集語系國家文字就可能沒有發現此類問題,否則就要看看Sybase是否還有出 EBF 可以修正這問題了。

在還沒有 EBF 可以修正以前,改用 ReadEx 指令會是比較正確的作法。

2012年8月10日 星期五

powerbuilder 的邏輯運算函數

powerbuilder 的位元運算能力幾乎同等『沒有』。

但是使用 windows API 又常常遇到許多變數需要邏輯運算過,但是看看目前的 PFC 的邏輯運算效能也不好。

因此突發奇想,把原本 PFC 的邏輯運算改一改,發現也蠻好用的,效能上也有改善。

Function名稱:f_bitoperator
回傳值:unsignedlong
傳遞參數:aul_s1 [unsignedlong][value]、aul_s2 [unsignedlong][value]、as_operator [string][value]

/* PFC簡易版兩組數字做邏輯運算 輸入來源:aul_s1 [uLong],aul_s2 [uLong],as_operator [String] as_operator為邏輯運算描述如:"and" , "or" , "xor" , "not"只需要第一組(aul_s1)輸入, aul_s2放0就可以了 返回:運算結果[Long],當參數錯誤返回NULL */ uLong ll_ret boolean lb_s1[32] , lb_s2[32] , lb_u integer li_bit as_operator = lower(trim(as_operator)) //參數檢查 if isnull(aul_s1) or isnull(aul_s2) or isnull(as_operator) then setnull(ll_ret) Return ll_ret end if if as_operator = "" then setnull(ll_ret) Return ll_ret end if //位元轉換 for li_bit = 32 to 1 step -1 if mod(aul_s1 , 2) = 1 then lb_s1[li_bit] = true else lb_s1[li_bit] = false end if aul_s1 = truncate(aul_s1 / 2 , 0) next if as_operator <> "not" then for li_bit = 32 to 1 step -1 if mod(aul_s2 , 2) = 1 then lb_s2[li_bit] = true else lb_s2[li_bit] = false end if aul_s2 = truncate(aul_s2 / 2 , 0) next end if //邏輯運算 ll_ret = 0 for li_bit = 32 to 1 step -1 choose case as_operator case "and" lb_u = lb_s1[li_bit] and lb_s2[li_bit] case "or" lb_u = lb_s1[li_bit] or lb_s2[li_bit] case "xor" lb_u = not (lb_s1[li_bit] or lb_s2[li_bit]) case "not" lb_u = not lb_s1[li_bit] end choose if lb_u then ll_ret += 2^(32 - li_bit) next Return ll_ret

雖然說可以用 VC++ 寫一個支援的 DLL 來作邏輯運算,但是殺雞用牛刀好像小題大作了。

其次,在某些特別環境下用 VC++ 寫的 DLL 要重新編譯過,例如:Pocket Powerbuilder 在做手持終端設備開發就不太方便了。

這次改的部份大致有兩點:一、邏輯儲存使用 Boolean 陣列,節省轉換與記憶體運算空間,二、這個 Function 就可以做到 AND、OR、XOR、NOT 四種運算了,使用上很方便。

另外,就是 in/out 使用 UnsignedLong 長達 32 bit 的運算,即使你是傳遞 integer、long 都沒問題,
負數也是可以邏輯運算的。 ^_^

2012年8月9日 星期四

匯入 EDI 檔案到 DataStore

在台灣寫程式常常會需要使用到其他公司的資料交換,而大部份的合作夥伴都很多都會使用EDI檔案傳遞,因此處理EDI檔案就成了家常便飯,但有時候又不想要遇到一個專案就要再寫一次,所以幾乎都將它轉成PFC來使用。

上一次提到了 EDI 轉出 Function,所以這次來看看轉入EDI檔案的 Function怎麼做吧。

Function 名稱:f_edi2ds
回傳值:Long
傳遞參數:ads_dest[datastore][refrence]、as_file[string][readonly]、as_error[string][refrence]

/* 將ED文字檔(as_file)I匯入到 DataStore(ads_dest) EDI切割模式依照 dataObject 的 column.limit 訂定的長度 */ Integer li_ColCount,li_Pos,li_Col,li_file Long ll_collimit[],ll_rc,ll_totl,ll_cc,ll_new String ls_colname[],ls_coltype[],ls_data,ls_val,ls_date,ls_time Datetime ldt_1 boolean lb_fin //檔案存在否? IF not FileExists(as_file) THEN Return -1 //DataStore存在否? IF isnull(ads_dest) OR not isValid(ads_dest) THEN Return -1 END IF //欄位存在否? li_ColCount = integer(ads_dest.Describe("datawindow.column.count")) IF li_ColCount <= 0 THEN Return -1 ll_totl = 0 //取得欄位屬性 FOR li_Col=1 TO li_ColCount ls_colname[li_Col] = ads_dest.Describe("#" + string(li_Col) + ".Name") ll_collimit[li_Col] = Integer(ads_dest.Describe("#" + string(li_Col) + ".Edit.Limit")) //合計欄位總長度 ll_totl += ll_collimit[li_Col] ls_coltype[li_Col] = lower(ads_dest.Describe("#" + string(li_Col) + ".ColType")) //排除括弧字元 li_Pos = Pos(ls_coltype[li_Col] , "(") IF li_Pos > 0 THEN ls_coltype[li_Col] = Left(ls_coltype[li_Col] , li_Pos - 1) END IF NEXT //開啟檔案(以LinMode讀取) li_file = FileOpen(as_file , LineMode! , Read! ,LockReadWrite!) IF li_file <= 0 THEN Return -2 ll_cc = 0 //逐行讀取 do while fileRead(li_file , ls_data) >= 0 //行數計數 ll_cc ++ //筆對長度 IF len(ls_data) < ll_totl THEN as_error += "第" + String(ll_cc) + "行長度過少:" + String(len(ls_data)) +& "<" + String(ll_totl) + "~r~n" Continue END IF //新增一筆ROW到DataStore ll_new = ads_dest.InsertRow(0) li_pos = 1 //定位歸0 lb_fin = true //錯誤標記歸0 FOR li_col = 1 TO li_ColCount //讀取指定位置範圍字串 ls_val = MidA(ls_data ,li_pos , ll_collimit[li_Col]) //依照欄位類型轉換 Choose Case ls_coltype[li_Col] Case "char" //文字 IF trim(ls_val) = "" THEN SetNull(ls_val) ads_dest.SetItem(ll_new , li_col , ls_val) Case "datetime" //日期時間 IF trim(ls_val) <> "" THEN ls_date = left(ls_val,4)+"/"+mid(ls_val,5,2)+"/"+mid(ls_val,7,2) IF not isDate(ls_date) THEN as_error += "第"+String(ll_cc)+"行,位置:"+String(li_pos)+",長度:"+& String(ll_collimit[li_Col])+"無法轉換Datetime~r~n" lb_fin = false EXIT END IF ls_time = left(mida(ls_val , 9)+"000000",6) ls_time = left(ls_time,2)+":"+mid(ls_time,3,2)+":" +mid(ls_time,5,2) IF not isTime(ls_time) THEN as_error += "第"+String(ll_cc)+"行,位置:"+String(li_pos)+",長度:"+& String(ll_collimit[li_Col])+"無法轉換Datetime~r~n" lb_fin = false EXIT END IF ldt_1 = Datetime(date(ls_date),time(ls_time)) ELSE SetNull(ldt_1) END IF ads_dest.SetItem(ll_new , li_col , ldt_1) Case "date" //日期 IF trim(ls_val) <> "" THEN ls_date = left(ls_val,4)+"/"+mid(ls_val,5,2)+"/"+mid(ls_val,7,2) IF not isDate(ls_date) THEN as_error += "第"+String(ll_cc)+"行,位置:"+String(li_pos)+",長度:"+& String(ll_collimit[li_Col])+"無法轉換Date~r~n" lb_fin = false EXIT END IF ELSE SetNull(ls_date) END IF ads_dest.SetItem(ll_new , li_col , Date(ls_date)) Case "time" //時間 IF trim(ls_val) <> "" THEN ls_time = left(ls_val+"000000",6) ls_time = left(ls_time,2)+":"+mid(ls_time,3,2)+":"+mid(ls_time,5,2) IF not isTime(ls_time) THEN as_error += "第"+String(ll_cc)+"行,位置:"+String(li_pos)+",長度:"+& String(ll_collimit[li_Col])+"無法轉換Time~r~n" lb_fin = false EXIT END IF ELSE SetNull(ls_time) END IF ads_dest.SetItem(ll_new , li_col , Time(ls_time)) Case "long" ,"ulong" //整數 IF trim(ls_val) = "" THEN SetNull(ls_val) END IF ads_dest.SetItem(ll_new , li_col , long(ls_val)) Case "decimal" //數值 IF trim(ls_val) = "" THEN SetNull(ls_val) END IF ads_dest.SetItem(ll_new , li_col , dec(ls_val)) Case "real" //浮點數 IF trim(ls_val) = "" THEN SetNull(ls_val) END IF ads_dest.SetItem(ll_new , li_col , real(ls_val)) End Choose //定位點移到下一個指定位置 li_pos = li_pos + ll_collimit[li_Col] NEXT //處理錯誤跳出,未完成者取消此筆ROW IF lb_fin = false THEN ads_dest.rowsDisCard(ll_new , ll_new , Primary!) END IF loop //關閉檔案 FileClose(li_file) //回傳處理行數 Return ll_cc
與之前轉出EDI作法有點不一樣的地方,是在於取得 DataObject 的 DataWindow 欄位屬性時,我直接使用了幾個陣列(ll_collimit[]、ls_colname[]、ls_coltype[])來儲存,上次是使用一個structure來儲存,結果是相同,寫法不同,可以提供一個比較參考。

其次,這次轉入EDI檔案時欄位長度的參考,我是設計在每個欄位的 Edit.Limit 上,所以這個Function 使用時只要設計好 DataWindow 的模型就很好用了。

其次,使用上需要特別注意的地方是[日期時間]、[日期]、[時間]的轉換方式,我的設計上是假設EDI上的欄位日期是西元年的 YYYYMMDDHHMMSS 的格式的,所以 Datetime
欄位的 Limit 可以設定 8、10、14、16碼的,怎麼說呢?

假設日期欄來源資料為 YYMMDDHHMM的話,後面SS會自動補上00
例如:201208091325 -> 20120809132500 -> 2012/8/9 13:25:00
例如:2012080913 -> 20120809130000 -> 2012/8/9 13:00:00
如此可以簡化不少操作

倘偌需要更多格式的日期轉換就需要改寫轉換規則就可以了。