
私のアプリ「Sums Up」では正規表現を使ってテキストフィールド内の文字列から数字だけを検知して、その合計を表示するということをやっています。計算機とメモ帳を行ったり来たりする必要がこのアプリを使うとなくなります。
https://appsto.re/jp/FBd-6.i
訳あってこのアプリをObjective CからSwiftに完全移行したのですが、エラーは消えたものの問題が一つ発生してしまいました。アラビア語キーボードで数字が入力されると落ちてしまうのです。
正常に動いていたObjective Cのコード
大まかな流れとしてはまずpatternというNSStringオブジェクトに正規表現式を入れて、文字列全体から数字を検索します。
NSString *pattern = [[NSString alloc]initWithFormat:@”(正規表現式は省略させてください)”,_thousandSeparatorForRegEx,_decimalCharacterForRegEx,_thousandSeparatorForRegEx];
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:NULL];
_thousandSeparatorForRegExの中身は@”,”か@”.”。これは地域によっては数字の千を1,000ではなく1.000と書くためです。
_decimalCharacterForRegExも同様の趣旨です。小数点をコンマ(,)で書く地域に対応させます。
for (int i = 0; i < paragraphs.count; i++) {
NSString *thisLine = [paragraphs objectAtIndex:i];
NSArray *words = [thisLine componentsSeparatedByString:@” “];
NSMutableAttributedString *thisLineWithAttribution = [[NSMutableAttributedString alloc]init];
forループでパラグラフ、つまり一行ごとに切り分けた文字列をthisLineに格納し、さらに空白で分割してwordsという配列に入れました。thisLineWithAttributionは後で文字に色を付けるためのものですが今回のテーマとは関係ないので省略します。
for (int i = 0 ; i < words.count ; i++) {
NSArray *matches = [regex matchesInString:word options:0 range:NSMakeRange(0, [word length])];
if (matches.count != 0) {
for (int j = 0; j < matches.count; j++) {
NSTextCheckingResult *match = [matches objectAtIndex:j];
NSString *matchString = [word substringWithRange:match.range];
NSString *numberPart = [regex stringByReplacingMatchesInString:matchString options:0 range:NSMakeRange(0, [matchString length]) withTemplate:@”$3″];
double thisNumber = [[[[numberPart stringByReplacingOccurrencesOfString:_thousandSeparator withString:@””]stringByReplacingOccurrencesOfString:@”\n” withString:@””] stringByReplacingOccurrencesOfString:_decimalCharacter withString:@”.”]
doubleValue];
さらにwordごとに巡回するforループを中に作ります。そして正規表現に当てはまるもの全てをmatchesに格納します。そしてmatchesの各要素を再度forループでmatchに格納して、ようやく数字部分の【文字列の】抽出が完了です。matchからmatchStringというNSStringに変換して、さらに$3、つまりテンプレートの三番目の要素のみを取り出します。テンプレートは例えば「-$1,234.56」という文字列があったときに
- $1 = – (マイナス記号)
- $2 = $ (通貨記号)
- $3 = 1,234.56 (数字部分)
というように切り分けるようになっています。thisNumberには最後の数字部分からさらにコンマを取り除き、小数点をピリオド(.)に統一したものを文字列として代入します。
この方法で、日本における1,234.56も、ヨーロッパにおける1.234,56も、アラビア語圏における١ ٢ ٣ ٤ .٥ ٦も正しく「1234.56」に変換されていました。
Swift変換直後のコード
問題になったのは上のコードの最後の一行、thisNumberの部分です。Swiftでは当初次のように記述しました。
let thisNumber = Double(numberPart?.replacingOccurrences(of: thousandSeparator, with: “”).replacingOccurrences(of: “\n”, with: “”).replacingOccurrences(of: decimalCharacter, with: “.”) ?? “0”)!
やろうとしていることは上と同じでnumberPartというStringからthousandSeparatorを取り去って、小数点をピリオドで置き換えようというものです。最後についている
?? “0”
はXcodeの警告メッセージに従って加えた変更です。一番最後の
)!
も同様です。最初はエラーが消えたからまあいいかぐらいに考えていました。
Swift独自のコンセプト、?と!
しかし、Objective Cからやってきた私にはこの?と!が曲者でした。Swiftのインスタンスにオプショナル型(optional)とそうでない型の2つがあることはぼんやり知っており、Objective Cのようにnilをすんなり渡せないことは知っていました。だから、うっかりnilを渡さないために
例) let text = someString ?? “”
オプショナル型のsomeStringがもしnilだったら初期値””をtextに代入せよ
とか
例) let text2 = someString!
someStringは絶対にnilではないから強制的にアンラップ(unwrap)してオプショナル型を解除せよ
というように処理したのでした。実際、上のSwiftコードで1234567890は認識できていたのです。
※ちなみにただunwrapするだけならsomeString?で大丈夫です。この場合はnilが返ることがあるので要注意ですが
アラビア語キーボードへの対応
本記事冒頭のスクリーンショットのキーボード一番上の段を見てもらうと分かりますが、アラビア語キーボードにおける数字は我々が使っている数字とは異なります。1、2、3と9と0は似てますね。なるほど、アラビア語が由来だからアラビア数字というのかと思いましたが、どちらも呼び名がアラビア数字なのが困りものです。が、それはさておき。
上述のthisNumberの式の最後で!で強制アンラップすると、アラビア語キーボードの数字のときだけクラッシュしてしまうということが、ユーザーさんからの報告で分かりました。
では!をやめて、thisNumberをオプショナル型にすればいいのでは、と思ったのですが、それも問題がありました。一旦Doubleに変換したthisNumberを後で全て足し算するために再びString型の変数に変換しなければならないのですが、
String(format: format, thisNumber)
この式がthisNumberがオプショナル型だと処理を受け付けてくれないのです。
解決策
結局次のように書き換えることで解決しました。
let thisNumberReplacedThousandAndDecimal = numberPart?.replacingOccurrences(of: thousandSeparator, with: “”).replacingOccurrences(of: “\n”, with: “”).replacingOccurrences(of: decimalCharacter, with: “.”)
var thisNumber : Double = 0.0
if let thisNumberReplacedThousandAndDecimal = thisNumberReplacedThousandAndDecimal {
let Formatter: NumberFormatter = NumberFormatter()
Formatter.locale = NSLocale(localeIdentifier: “EN”) as Locale?
let final = Formatter.number(from: thisNumberReplacedThousandAndDecimal)
if final != 0 {
thisNumber = Double(truncating: final!)
}
}
つまりアラビア語キーボードの数字をUSロケールの1234567890に置き換えてから強制案ラップするというやりかたです。
実は全角数字でも同じ処理が必要だった
あとから分かったことですが、日本語キーボードで全角数字を入れた場合でも、この修正を加えないとやはり落ちてしまうのでした。正規表現上は数字として拾い上げられるが、数字には変換できない文字種が複数あるということですね。
最初は解決策がなかなかみつからず、いっそ0〜9までの文字を一個ずつ置き換えて取り急ぎのクレーム対策としようかとも思ったのですが、世界中で使われている自分のアプリが一体何語で使われているか見当もつきませんから、クレームのたびにアップデートするのはやはり得策ではない。頑張って正しい処理方法が見つけることができてよかったです。
それにしても、日本のユーザーさんからクレームが来る前に、アラビア語圏の方が先に教えてくれて本当にラッキーでした(その方の優しい口調にも救われました)。いずれにせよ、自分のアプリが世界中で使われていると思うと嬉しいですね。これからもがんばります!