Open2

[Rails] ハイフンもどき一掃計画 ~ハイフンに似たやつらを置換する~

juny@tajuny@ta

スマホアプリでエンドユーザに住所や電話番号を入力してもらうようなサービスを運用していると、ハイフンの入力が自由すぎて困ることがあった。

具体的にサービス内で顧客リストをCSV化してS-JISで作成する必要がある時などに、文字コードの問題で変換できなかったり、番地のパース処理等で漏れたりいろいろ厄介のタネになる。

またエンドユーザは「よこぼう」であればなんでもいいと思ってるらしく、中にはアンスコ(_: U+005F)やチルダ(~: U+007E)なんかも半角・全角で混ざってる。(チルダでも波ダッシュ「〜: U+301C」と全角チルダ「~: U+FF5E」の違いあったりで何がなんだか...)

そうやってひとまずハイフンもどき系 + アンスコ系 + オーバーライン系 + チルダ系を洗い出すと以下のようなバリエーションがあるよう。
他にもありそうだがこれでおおよそカバーはできそう。

「-˗ᅳ᭸‐‑‒–—―⁃⁻−▬─━➖ーㅡ﹘﹣-ー𐄐𐆑__‾ ̄~〜~」

なのでこれらの文字が混ざった場合は強制的にハイフン(-: U+002D)に変換してコミットするように対応する例を残しておく。

一例として、app/services 配下にモジュールとして Utils::StringConverter を作成

  • app/services/utils/string_converter.rb
module Utils
  module StringConverter
    module_function

    # ハイフンに似た文字列をハイフンに置換する
    # excludes => 置換の対象としない文字列
    def normalize_hyphens(str, excludes: nil)
      return '' if str.nil?

      pattern = /[˗ᅳ᭸‐‑‒–—―⁃⁻−▬─━➖ーㅡ﹘﹣-ー𐄐𐆑__‾ ̄~〜~]/
      pattern = /#{pattern.to_s.delete(excludes)}/ if excludes

      str.gsub(pattern, '-')
    end
  end
end

任意のモデルでコールバック等利用して変換をかける

  • app/models/user.rb
class User < ApplicationRecord
  # 省略
  before_validate do
    self.phone_number = Utils::normalize_hyphens(phone_number) unless phone_number.blank?
    self.postal_code = Utils::normalize_hyphens(postal_code) unless postal_code.blank?
    unless address_line.blank?
      # 住所向けにはカタカナの長音を除くなど
      self.address_line = Utils::normalize_hyphens(address_line, excludes: 'ーー')
    end
  end
  # 省略
end

ちなみに各記号毎のUnicode番号、Unicode称号は以下の通り(GPT先生に洗い出してもらった)

記号 Unicode番号 Unicode称号
- U+002D HYPHEN-MINUS
˗ U+02D7 MODIFIER LETTER MINUS SIGN
U+1173 HANGUL JUNGSEONG EU
U+1B78 BALINESE MUSICAL SYMBOL LEFT-HAND OPEN PANG
U+2010 HYPHEN
U+2011 NON-BREAKING HYPHEN
U+2012 FIGURE DASH
U+2013 EN DASH
U+2014 EM DASH
U+2015 HORIZONTAL BAR
U+2043 HYPHEN BULLET
U+207B SUPERSCRIPT MINUS
U+2212 MINUS SIGN
U+25AC BLACK RECTANGLE
U+2500 BOX DRAWINGS LIGHT HORIZONTAL
U+2501 BOX DRAWINGS HEAVY HORIZONTAL
U+2796 HEAVY MINUS SIGN
U+30FC KATAKANA-HIRAGANA PROLONGED SOUND MARK
U+3161 HANGUL LETTER EU
U+FE58 SMALL EM DASH
U+FE63 SMALL HYPHEN-MINUS
U+FF0D FULLWIDTH HYPHEN-MINUS
U+FF70 HALFWIDTH KATAKANA-HIRAGANA PROLONGED SOUND MARK
𐄐 U+10110 AEGEAN NUMBER TEN
𐆑 U+10191 ROMAN UNCIA SIGN
_ U+005F LOW LINE
_ U+FF3F FULLWIDTH LOW LINE
U+203E OVERLINE
U+FFE3 FULLWIDTH MACRON
~ U+007E TILDE
U+FF5E FULLWIDTH TILDE
U+301C WAVE DASH
juny@tajuny@ta

住所については「1 ➖ 1 〜 1 パークコートタワー101」のような文字列を考慮してカタカナの長棒などは対象外とするようにした

こちらの対応について、数字の間にあるハイフンもどきだけを対象に変換をかけられるよう、住所専用のメソッドを設けることにした。

module Utils
  module StringConverter
    module_function

    # ハイフンに似た文字列をハイフンに置換する
    # excludes => 置換の対象としない文字列
    def normalize_hyphens(str, excludes: nil)
      return '' if str.nil?

      pattern = /[˗ᅳ᭸‐‑‒–—―⁃⁻−▬─━➖ーㅡ﹘﹣-ー𐄐𐆑__‾ ̄~〜~]/
      pattern = /#{pattern.to_s.delete(excludes)}/ if excludes

      str.gsub(pattern, '-')
    end

+   # 住所に含まれる文字列を考慮したハイフンノーマライズ
+   # 建物名に含まれる長音は残しつつ、それ以外のハイフンに似た文字列をハイフンに置換する
+   # ex. 1ー1 〜 1➖ 1 パークコート101 => 1-1-1-1 パークコート101
+   def normalize_hyphens_in_address(str)
+     # カタカナの長音(全角/半角)を除外してノーマライズ
+     str = normalize_hyphens(str, excludes: 'ーー')
+
+     # 更に数字間に残ったカタカナの長音(全角/半角)をハイフンに置換しつつ、ついでにスペース削除
+     pattern = /(?<=[0-90-9])[  ]*[-ーー]+[  ]*(?=[0-90-9])/
+     str.gsub(pattern, '\1-\2')
+   end
  end
end
  • (?<=[0-90-9]) =>「直前に数字(全角含む)がある」
  • [  ]* => 「半角スペース/全角スペースが0回以上続く(あってもなくてもマッチ)」
  • [-ーー]+ => 「[]内の記号が一回以上」
  • (?=[0-90-9]) => 「直後に数字(全角含む)がある」
  • \1-\2 => 「(?<=\d|[0-9])で一致した箇所-(?=\d|[0-9])で一致した箇所」となるよう置換