2023/10/06

[CB][Delphi]解決跨國小數點符號不同的程式處理

作者:吳祐賓





多數歐洲國家的數字浮點數,會使用逗號「,」作為小數點基點。

這陣子接獲客戶反應他們開發的軟體遇上 "Invalid floating point operation" 的錯誤

雖然最後並沒有採用程式解法來撫平此問題,只是這解決過程太有趣,一定要和你分享!



現象和解法

現象很簡單,重現很困難,因為在客戶說在他的環境下無法重現此錯誤。

經過一段時間的了解,才知道應用程式是在越南使用。所以製作越南版 Windows 後就成功重現此問題。而作為曾經的法國殖民地,法國也是以逗號作為基點。

在這裡我們就進行基點的轉換來處理,以 GetLocaleInfo Win32 API 來查出基點浮號進行轉換,程式碼如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Delphi
function GetLocaleDecimalMark: string;
var
  pcLCA: array[0..18] of Char;
begin
  if GetLocaleInfo(LOCALE_SYSTEM_DEFAULT, LOCALE_SDECIMAL, pcLCA, 19) <= 0 then
    pcLCA[0] := #0;
    
  Result := string(pcLCA);
end;


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// C++ Builder
// 取得本地端系統的小數點符號
// Function: GetLocaleDecimalMark
// Return: 小數點符號
String __fastcall GetLocaleDecimalMark()
{
    char pcLCA[19]; // 建立一個容納小數點符號的字元陣列

    // 呼叫 GetLocaleInfo 函式取得系統預設的小數點符號
    // LOCALE_SYSTEM_DEFAULT 表示使用本地端系統的設定
    // LOCALE_SDECIMAL 是代表小數點符號的常數
    // pcLCA 用來儲存取得的小數點符號
    if (GetLocaleInfo(LOCALE_SYSTEM_DEFAULT, LOCALE_SDECIMAL, pcLCA, 19) <= 0)
    {
        pcLCA[0] = '\0'; // 若取得失敗,將陣列第一個元素設為空字元
    }

    return String(pcLCA); // 將字元陣列轉換為 String 並返回
}


GetLocaleInfo 可以做得更多

Win32 API GetLocaleInfo 可以取得多樣化的系統資訊,經過包裝後可以使用以下程式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Delphi

// ShowMessage(GetLocaleInformation(LOCALE_SDECIMAL));
function GetLocaleInformation(iFlag: Integer): string;
var
  pcLCA: array[0..18] of Char;
begin
  if GetLocaleInfo(LOCALE_SYSTEM_DEFAULT, iFlag, pcLCA, 19) <= 0 then
    pcLCA[0] := #0;
    
  Result := string(pcLCA);
end;


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// C++ Builder
#include <System.SysUtils.hpp>

// ShowMessage(GetLocaleInformation(LOCALE_SDECIMAL));
String __fastcall GetLocaleInformation(int iFlag)
{
    char pcLCA[19]; // 建立一個容納結果的字元陣列

    if (GetLocaleInfo(LOCALE_SYSTEM_DEFAULT, iFlag, pcLCA, 19) <= 0)
    {
        pcLCA[0] = '\0'; // 若取得失敗,將陣列第一個元素設為空字元
    }

    return String(pcLCA); // 將字元陣列轉換為 AnsiString 並返回
}

iFlag 在 windows 單元有定義常數,可以依照自己的需求設定。常數列表如下:

LOCALE_NOUSEROVERRIDE { 不使用使用者自訂設定 }
LOCALE_USE_CP_ACP { 使用系統ACP }
LOCALE_ILANGUAGE { 語言代碼 }
LOCALE_SLANGUAGE { 本地語言名稱 }
LOCALE_SENGLANGUAGE { 語言的英文名 }
LOCALE_SABBREVLANGNAME { 語言名稱縮寫 }
LOCALE_SNATIVELANGNAME { 本地語言名稱 }
LOCALE_ICOUNTRY { 國家代碼 }
LOCALE_SCOUNTRY { 國家名 }
LOCALE_SENGCOUNTRY { 國家的英文名稱 }
LOCALE_SABBREVCTRYNAME { 國家名縮寫 }
LOCALE_SNATIVECTRYNAME { 國家名 }
LOCALE_IDEFAULTLANGUAGE { 預設語言代碼 }
LOCALE_IDEFAULTCOUNTRY { 預設國家代碼 }
LOCALE_IDEFAULTCODEPAGE { 預設OEM內碼表 }
LOCALE_IDEFAULTANSICODEPAGE { 預設ANSI內碼表 }
LOCALE_IDEFAULTMACCODEPAGE { 預設MAC頁 }
LOCALE_SLIST { 列表項分隔符 }
LOCALE_IMEASURE { 測量單位 0 = 公制, 1 = 英制 }
LOCALE_SDECIMAL { 小數點符號 }
LOCALE_STHOUSAND { 千位分隔符 }
LOCALE_SGROUPING { 數字分組 }
LOCALE_IDIGITS { 小數位數 }
LOCALE_ILZERO { 小數前導零 }
LOCALE_INEGNUMBER { 負數模式 }
LOCALE_SNATIVEDIGITS { 本地ASCII數字 0-9 }
LOCALE_SCURRENCY { 本地貨幣符號 }
LOCALE_SINTLSYMBOL { 國際貨幣符號 }
LOCALE_SMONDECIMALSEP { 貨幣小數點分隔符 }
LOCALE_SMONTHOUSANDSEP { 貨幣千位分隔符 }
LOCALE_SMONGROUPING { 貨幣分組 }
LOCALE_ICURRDIGITS { 本地貨幣位數 }
LOCALE_IINTLCURRDIGITS { 國際貨幣位數 }
LOCALE_ICURRENCY { 正貨幣模式 }
LOCALE_INEGCURR { 負貨幣模式 }
LOCALE_SDATE { 日期分隔符 }
LOCALE_STIME { 時間分隔符 }
LOCALE_SSHORTDATE { 短日期字串 }
LOCALE_SLONGDATE { 長日期字串 }
LOCALE_STIMEFORMAT { 時間格式字串 }
LOCALE_IDATE { 短日期格式順序 }
LOCALE_ILDATE { 長日期格式順序 }
LOCALE_ITIME { 時間格式指示符 }
LOCALE_ITIMEMARKPOSN { 時間標記位置 }
LOCALE_ICENTURY { 世紀格式指示符 (短日期) }
LOCALE_ITLZERO { 時間欄位前導零 }
LOCALE_IDAYLZERO { 日欄位前導零 (短日期) }
LOCALE_IMONLZERO { 月欄位前導零 (短日期) }
LOCALE_S1159 { 上午標誌 }
LOCALE_S2359 { 下午標誌 }
LOCALE_ICALENDARTYPE { 日曆類型指示符 }
LOCALE_IOPTIONALCALENDAR { 附加日曆類型指示符 }
LOCALE_IFIRSTDAYOFWEEK { 一星期的第一天指示符 }
LOCALE_IFIRSTWEEKOFYEAR { 一年的第一周指示符 }
LOCALE_SDAYNAME1 { 星期一的完整名稱 }
LOCALE_SDAYNAME2 { 星期二的完整名稱 }
LOCALE_SDAYNAME3 { 星期三的完整名稱 }
LOCALE_SDAYNAME4 { 星期四的完整名稱 }
LOCALE_SDAYNAME5 { 星期五的完整名稱 }
LOCALE_SDAYNAME6 { 星期六的完整名稱 }
LOCALE_SDAYNAME7 { 星期天的完整名稱 }
LOCALE_SABBREVDAYNAME1 { 星期一的簡稱 }
LOCALE_SABBREVDAYNAME2 { 星期二的簡稱 }
LOCALE_SABBREVDAYNAME3 { 星期三的簡稱 }
LOCALE_SABBREVDAYNAME4 { 星期四的簡稱 }
LOCALE_SABBREVDAYNAME5 { 星期五的簡稱 }
LOCALE_SABBREVDAYNAME6 { 星期六的簡稱 }
LOCALE_SABBREVDAYNAME7 { 星期天的簡稱 }
LOCALE_SMONTHNAME1 { 一月的完整名稱 }
LOCALE_SMONTHNAME2 { 二月的完整名稱 }
LOCALE_SMONTHNAME3 { 三月的完整名稱 }
LOCALE_SMONTHNAME4 { 四月的完整名稱 }
LOCALE_SMONTHNAME5 { 五月的完整名稱 }
LOCALE_SMONTHNAME6 { 六月的完整名稱 }
LOCALE_SMONTHNAME7 { 七月的完整名稱 }
LOCALE_SMONTHNAME8 { 八月的完整名稱 }
LOCALE_SMONTHNAME9 { 九月的完整名稱 }
LOCALE_SMONTHNAME10 { 十月的完整名稱 }
LOCALE_SMONTHNAME11 { 十一月的完整名稱 }
LOCALE_SMONTHNAME12 { 十二月的完整名稱 }
LOCALE_SMONTHNAME13 { 第十三個月的完整名稱 (若存在) }
LOCALE_SABBREVMONTHNAME1 { 一月的簡稱 }
LOCALE_SABBREVMONTHNAME2 { 二月的簡稱 }
LOCALE_SABBREVMONTHNAME3 { 三月的簡稱 }
LOCALE_SABBREVMONTHNAME4 { 四月的簡稱 }
LOCALE_SABBREVMONTHNAME5 { 五月的簡稱 }
LOCALE_SABBREVMONTHNAME6 { 六月的簡稱 }
LOCALE_SABBREVMONTHNAME7 { 七月的簡稱 }
LOCALE_SABBREVMONTHNAME8 { 八月的簡稱 }
LOCALE_SABBREVMONTHNAME9 { 九月的簡稱 }
LOCALE_SABBREVMONTHNAME10 { 十月的簡稱 }
LOCALE_SABBREVMONTHNAME11 { 十一月的簡稱 }
LOCALE_SABBREVMONTHNAME12 { 十二月的簡稱 }
LOCALE_SABBREVMONTHNAME13 { 第十三個月的簡稱 (若存在) }
LOCALE_SPOSITIVESIGN { 正數符號 }
LOCALE_SNEGATIVESIGN { 負數符號 }
LOCALE_IPOSSIGNPOSN { 正數符號位置 }
LOCALE_INEGSIGNPOSN { 負數符號位置 }
LOCALE_IPOSSYMPRECEDES { 貨幣符號在正數前面 }
LOCALE_IPOSSEPBYSPACE { 貨幣符號和正數之間用空格分隔 }
LOCALE_INEGSYMPRECEDES { 貨幣符號在負數前面 }
LOCALE_INEGSEPBYSPACE { 貨幣符號和負數之間用空格分隔 }
LOCALE_FONTSIGNATURE { 字型簽名 }
LOCALE_SISO639LANGNAME { ISO 語言簡稱 }
LOCALE_SISO3166CTRYNAME { ISO 國家簡稱 } 


最後客戶採用解法

客戶最終採用強制指定符號顯示方式,結束這一回合。



結論

本案中透過程式技術上確實可以解決問題,只是解決過程曠日費時,而且專案修改上有一定程度上的困難,光是每次的運算處理就必須經過一道檢查,檢查點一多就難保有未改到或是有其它 Bug 產生的可能性。

在經過人的斡旋後,發現對方能接受調整基點的顯示方式,如此就能輕易解決程式難以處理的場合。誰說技術只限定在程式上?人和人之間的互動也是一門技術上的延伸。經過這次的經歷讓我了解到程式人員除了專研自身程式技術外,和人的互動也是一門必修的程式延伸課程。您說是吧?

和您分享。


作者新書在這裡



See also



沒有留言:

張貼留言