社内用機種依存文字変換検出ライブラリを作った話

はじめに

こんにちは。id:takasp です。
この記事は airCloset Advent Calendar 2021 の 3 日目です。

さて、他社サービスとの連携において住所情報などを連携すると、連携するサービスによっては機種依存文字の対応を求められる場合があります。
そんなときにどのように対応したかというのが今回のお話です。

下記ツイートは筆者が「こないだやったやつ!」と見たときに思ったツイートです。
このように 1 例としてハイフンに似た文字が混じると他社サービスとの連携に失敗するため、何らかの対策を講じる必要があります。

背景と対応方針

配送業者を通じてお客様にお洋服をお届け、または、返却をしていただく際にお客様の住所情報を配送業者に連携する必要があります。
しかし、配送業者へ連携する際に対応していない機種依存文字が含まれているとエラーになってしまい、発送・返却できないという事象が発生していました。
そこでお客様が住所情報を入力するタイミングで機種依存文字の対策をしていくことにしました。
ただ複数のサービス、リポジトリごとに機種依存文字の対応をすると同じ実装をすることになってしまうので、ライブラリを作り各サービスはそのライブラリを利用するという形式にしました。
各サービスのフロントエンド・バックエンドは JavaScript で実装されているため、1 つの npm ライブラリを作り各リポジトリがライブラリを呼び出すという方針で進めます。
作成したライブラリは OSS として公開することも検討しましたが、他社サービスの機種依存文字の対応状況に依存しているため社内公開に留めています。

やりたいこと

主にやりたいことは以下のとおりです。

  • 配送業者が対応していない機種依存文字を対策したい
  • 一般的に機種依存文字となるハイフン、ダッシュ、マイナスなどは 1 つのハイフンに統一したい
  • 半角カタカナは全角カタカナにしたい(厳密に機種依存文字という定義から外れるが文字化けする可能性があるため)
  • ついでに絵文字も対策したい
  • npm ライブラリであること
  • 機種依存文字を検知した場合、空白に変換するか、エラー通知するのいずれかが選べること

これらに対応するライブラリの作成を進めます。

意識したこと

ライブラリを作るに当たって以下を意識しました。

  • 本番ビルド時には何らかのライブラリに依存しないで作る
    • 特にフロントエンドは可能な限り軽くしたいので標準の機能だけを利用した実装を目指す
  • テストを書きながら実装を書く
    • 要件は明確だが最初から具体的な実装イメージが湧いていなかったため、テストを書きながら実装し、設計を模索する

ハイフンのような文字

記載したツイートのようにハイフン、ダッシュ、マイナスなどの似たような文字はたくさんあります。
以下に一例を記載しますが、目視で見ても差分を判別するのは難しいです。

文字 Unicode 名称
- U+002D Hyphen-Minus
U+FE63 Small Hyphen-Minus
U+FF0D Fullwidth Hyphen-Minus
U+2011 Non-Breaking Hyphen
U+2010 Hyphen
U+2012 Figure Dash
U+2013 En Dash
U+2014 Em Dash
U+2015 Horizontal Bar

技術選定

npm ライブラリの作り方としては以下のような記事を参考に実装しました。

TypeScriptでnpmライブラリ開発ことはじめ

また型がある嬉しさから TypeScript とユニットテストとしてデファクトスタンダードな Jest を用いてテストを書きながら実装を進めました。

テストを書きながら実装する

テストを書きながらとありますが、いわゆるテスト駆動開発(TDD)で進めました。

テスト駆動開発は以下のようなサイクルで開発をしていく手法です。

  1. レッド:失敗するテストを書く
  2. グリーン:できる限り早く、テストに通るような最小限のコードを書く
  3. リファクタリング:コードの重複を除去する

今回はその流れの 1 例を記載します。

TODO リストを作る

例えばまず以下のような TODO リストを作って実装していきます。

TODO
======================

- [] 機種依存文字のハイフンを小文字のハイフンに変換する
  - [] 機種依存文字の「–(En Dash)」を小文字の「-(Hyphen-Minus)」に変換する
- [] 半角カタカナを全角カタカナに変換する
  - [] 半角カタカナ「カ」を全角カタカナ「カ」に変換する
- [] 機種依存文字を空白に変換する
  - [] 配送業者の機種依存文字の場合
    - [] 機種依存文字「①」を空白に変換する
  - [] 絵文字の場合
    - [] 絵文字「😀」を空白に変換する
- [] 機種依存文字をエラーにする
  - [] 配送業者の機種依存文字の場合
    - [] 機種依存文字「①」を指定するとエラーが発生する
  - [] 絵文字の場合
    - [] 絵文字「😀」を指定するとエラーが発生する

テストコードを書く

まずハイフンに対するテストを書くとして、仮に変換するメソッドを convert メソッドとした時、小文字のハイフンに変換されることを期待します。

describe('機種依存文字のハイフンを小文字のハイフンに変換する', () => {
  it('機種依存文字の「–(En Dash)」を小文字の「-(Hyphen-Minus)」に変換する', () => {
    expect(convert('–')).toEqual('-');
  })
})

テストを実行して失敗することを確認する

この状態で Jest でテストを実行します。
そうすると当たり前ですが、convert メソッドは存在しないためエラーが発生します。

実装を行う

まずは単純に成功になるテストを実装します。

export function convert(text: string): string {
  return '-';
}

テストを実行して成功することを確認する

この状態で Jest でテストを実行します。 そうすると単純な実装をしたのでテストは成功します。

リファクタリング

ここでリファクタリングをするフェーズになったので、例えば単純な実装であった convert メソッドを正規表現で置き換えるような処理に変更したりします。

export function convert(text: string): string {
    return text.replace(/[–]/g, '-');
}

この後は三角測量でテストケースを増やしたり、次の TODO リストに取り掛かるなどしてテスト駆動開発のサイクルを回しながら磨き上げていきました。

このようにして進めると仮に誤った実装をしてもテストがあるので気づけるという点と、自身が一番最初の利用者になるため、利用しやすいライブラリは何かを考えながら実装できるという嬉しさがあります。
最終的にテストコードを見れば、ライブラリがどんな機能を提供してくれるのか分かるという利点もあります。

さいごに

ここまで機種依存文字の変換検出ライブラリを作るまでの過程をざっくりとお伝えしました。
作ってみていかにハイフンに似た文字がたくさんあるか分かりましたし、npm ライブラリの作り方を知る良いきっかけになりました。
要件は比較的明確なのでテスト駆動開発のお題にもいかがでしょうか。

参考

ハイフンに似てる文字の文字コード
Unicodeにあるハイフン/マイナス/長音符/波線/チルダのコレクション | hydroculのメモ
テスト駆動開発(TDD)