文字列→塩基配列の相互変換ツールをつくってみた(アプリ版)

# はじめに
[この記事](https://qiita.com/am10/items/41d94d5175dbf63fc706)で公開したアプリの中身についてです。

[この記事(どこまでショボいアプリがAppleの審査に通るのか試してみた)](https://qiita.com/megumu-u/items/600b9187aa3fec6539af)をみてわりと機能が少なくてもアプリ公開できるのか！と思い正月休みにアプリをつくってみました。

[以前作ったこれ(文字列→塩基配列の相互変換ツールをつくってみた(PHP))](https://qiita.com/am10/items/63b1f747a630a2913841)をアプリにしてリリースしました。

# リリースしたアプリ
つくったアプリは有料です。（目指せ！！トータルダウンロード数25！！！）  

* Mac, iOS: ￥120  
* Android : ￥100

## Macアプリ
ターゲット：MacOS Catalina以降

[DNA変換](https://apps.apple.com/jp/app/dna%E5%A4%89%E6%8F%9B/id1494127578?mt=12)

## iOSアプリ
ターゲット：iOS13以降

[DNA変換](https://apps.apple.com/app/id1493994947)

## Androidアプリ
ターゲット：Android6.0以降

[DNA変換](https://play.google.com/store/apps/details?id=am10.dnaconverter)

## Web版
こんなアプリに金払いたくねぇよって人はぜひWeb版をどうぞ

[http://adventam10.php.xdomain.jp/dna/index.php](http://adventam10.php.xdomain.jp/dna/index.php)

2020/01/31追加

PHPの部分をJavaScriptで置き換えました！！（これでスマホでも広告はでない:tada:）

[http://adventam10.html.xdomain.jp/dna/](http://adventam10.html.xdomain.jp/dna/)

# アプリ概要
機能は極小で文字列⇔塩基配列を相互変換し、Twitterに投稿できるアプリです。(一応英語版もつくりました)

| 文字列->塩基配列 | 塩基配列->文字列 |
| --- | --- |
|![ios_dna_1](https://user-images.githubusercontent.com/34936885/72214914-fc030900-354e-11ea-91dc-e6c9b29bc654.png)|![ios_dna_2](https://user-images.githubusercontent.com/34936885/72214918-07563480-354f-11ea-865e-08496c7ed25e.png)|

## Macアプリ

Macアプリは一発で審査が通ったのでiOSと比べると機能が少ないです。

機能

* 文字列⇔塩基配列を相互変換
* Twitterに投稿機能
* 塩基配列の他アプリへの共有機能
* 塩基配列のテキストファイルへの書き出し機能
* 塩基配列のペーストボードへコピー機能

## iOSアプリ
iOSアプリは4回リジェクトされたので他と比べると機能が多いです。

機能

* 文字列⇔塩基配列を相互変換
* Twitterに投稿機能
* 塩基配列の他アプリへの共有機能
* 塩基配列のテキストファイルへの書き出し機能
* 塩基配列のペーストボードへコピー機能
* 塩基配列の履歴機能（10件まで）
* 音声入力機能

1回目の審査では共有機能が使えないけど？バグじゃね？って理由でリジェクトされたのですが、2回目の審査で下記が追加されました:scream:

```
Guideline 4.2 - Design - Minimum Functionality
```

履歴機能追加 -> リジェクト、音声入力機能追加 -> 通過:tada:

音声入力機能追加後にアプリをアップしようとすると下記のようなメールが来ました

```
ITMS-90683: Missing Purpose String in Info.plist - Your app's code references one or more APIs that access sensitive user data. The app's Info.plist file should contain a NSSpeechRecognitionUsageDescription key with a user-facing purpose string explaining clearly and completely why your app needs the data. Starting Spring 2019, all apps submitted to the App Store that access user data are required to include a purpose string. If you're using external libraries or SDKs, they may reference APIs that require a purpose string. While your app might not use these APIs, a purpose string is still required. You can contact the developer of the library or SDK and request they release a version of their code that doesn't contain the APIs. Learn more (https://developer.apple.com/documentation/uikit/core_app/protecting_the_user_s_privacy).
```

ローカライズ対応していたので Info.plist にキーは追加せずに InfoPlist.strings に下記のように記述していたのですがそれではダメなようです。

```
"NSSpeechRecognitionUsageDescription" = "音声入力するために必要です";
"NSMicrophoneUsageDescription" = "音声入力するために必要です";
```

Info.plist にもキー追加して同じように記述してやると通りました。Info.plist にも記載するとこちらの記載が優先されて InfoPlist.string の文字が表示されないと思ったのですがそうでもないようです。（ちゃんとローカライズされてました。）

## Androidアプリ
Androidアプリはあんまさわったことがなかったので、最小構成です。（がんばってiOSアプリを追従するようにします！）

機能

* 文字列⇔塩基配列を相互変換
* Twitterに投稿機能

## Web版
Webもほぼさわったことないので、最小構成です。（一応レスポンシブ対応はしてます。）

機能

* 文字列⇔塩基配列を相互変換
* Twitterに投稿機能

# アプリのコードについて
このアプリのきもは文字列⇔塩基配列なのですがそこのコードについてです。
ソースは全部 GitHub で公開してます。

* [iOS&Mac](https://github.com/adventam10/DNAConverter-iOS)
* [Android](https://github.com/adventam10/DNAConverter-android)
* [Web(PHP)](https://github.com/adventam10/DNAConverter-web)
* [Web(JavaScript)](https://github.com/adventam10/DNAConverter-web_vue)

## 方法
変換方法は間に16進数をかませてやってます。

最初は4進数に変換してそれぞれ [0, 1, 2, 3] -> [A, T, C, G] のように変換していたのですが、以前[コメント](https://qiita.com/am10/items/63b1f747a630a2913841)で 0⇔AA みたいに2文字ずつやれば16進数でいけるよと教えていただきました:heart_eyes:

### 文字列->塩基配列
1. 文字列 -> 2進数に変換
2. 2進数 -> 16進数に変換
2. 16進数 -> 塩基配列に変換（ATCG）

### 塩基配列->文字列
1. 塩基配列 -> 2文字ずつに分割
2. 分割文字列 -> 16進数に変換
3. 16進数 -> 2進数に変換
4. 2進数 -> 文字列に変換

## 変換コード
もっといい方法があればぜひ教えて下さい！！
### swift
swift では String の Extension で文字列から16進数への変換、16進数から2進数への変換、文字列を2文字ずつ分割する変数とメソッドをつくりました。（swift が一番めんどくさい感じになってしまいました...:cry:）

```swift:StringExtensions.swift
public extension String {
    // 16進数->2進数への変換
    var hexadecimal: Data? {
        var data = Data(capacity: count / 2)
        let regex = try! NSRegularExpression(pattern: "[0-9a-f]{1,2}", options: .caseInsensitive)
        regex.enumerateMatches(in: self, range: NSRange(startIndex..., in: self)) { match, _, _ in
            let byteString = (self as NSString).substring(with: match!.range)
            let num = UInt8(byteString, radix: 16)!
            data.append(num)
        }
        guard data.count > 0 else { return nil }
        return data
    }
    
    // 文字列->16進数の変換
    var hex: String {
        let data = self.data(using: .utf8)!
        return data.map { String(format: "%02X", $0)}.joined()
    }
    
    // 指定文字数で文字を分割する
    func splitInto(_ length: Int) -> [String] {
        var str = self
        for i in 0 ..< (str.count - 1) / max(length, 1) {
            str.insert(",", at: str.index(str.startIndex, offsetBy: (i + 1) * max(length, 1) + i))
        }
        return str.components(separatedBy: ",")
    }
}
```

```swift
private let dnaHexValues: [String: String] =
        ["AA": "0", "AT": "1", "AC": "2", "AG": "3",
         "TA": "4", "TT": "5", "TC": "6", "TG": "7",
         "CA": "8", "CT": "9", "CC": "a", "CG": "b",
         "GA": "c", "GT": "d", "GC": "e", "GG": "f"]

// 文字列->塩基配列
func convertToDNA(_ text: String?) -> Result<String, DNAConvertError> {
        if isEmptyText(text) {
            return .failure(.empty)
        }
        var result = text!.hex.lowercased()
        dnaHexValues.forEach { dna, hex in
            result = result.replacingOccurrences(of: hex, with: dna)
        }
        return .success(result)
    }

// 塩基配列->文字列
func convertToLanguage(_ text: String?) -> Result<String, DNAConvertError> {
        if isEmptyText(text) {
            return .failure(.empty)
        }
        if isInvalidDNA(text) {
            return .failure(.invalid)
        }
        let hex = text!.splitInto(2).compactMap { dnaHexValues[$0] }.joined()
        if hex.isEmpty {
            return .failure(.invalid)
        }
        if let data = hex.hexadecimal,
            let result = String(data: data, encoding: .utf8) {
            return .success(result)
        }
        return .failure(.invalid)
    }
```

### kotlin
kotlinが一番スッキリした感じにかけました。

```kotlin
val dnaHexValues = mapOf(
        "AA" to "0", "AT" to "1", "AC" to "2", "AG" to "3",
        "TA" to "4", "TT" to "5", "TC" to "6", "TG" to "7",
        "CA" to "8", "CT" to "9", "CC" to "a", "CG" to "b",
        "GA" to "c", "GT" to "d", "GC" to "e", "GG" to "f"
    )

// 文字列->塩基配列
fun convertToDNA(text: String?): String? {
        if (text.isNullOrEmpty()) {
            return null
        }
        val hex = text.toByteArray().map { b -> String.format("%02X", b) }.joinToString("")
        var result = hex.toLowerCase()
        dnaHexValues.forEach { (k, v) -> result = result.replace(v, k) }
        return result
    }

// 塩基配列->文字列
fun convertToLanguage(text: String?): String? {
        if (text.isNullOrEmpty()) {
            return null
        }
        if (isInvalidDNA(text)) {
            return null
        }
        var index = 0
        val strings: MutableList<String> = mutableListOf()
        while (index < text.length) {
            strings.add(text.substring(index, index+2))
            index += 2
        }
        val hex = strings.map { n -> dnaHexValues[n] }.joinToString("").toUpperCase()
        val result = ByteArray(hex.length / 2) { hex.substring(it * 2, it * 2 + 2).toInt(16).toByte() }
        return String(result)
    }
```

### PHP
PHPは変換のときにバックスラッシュいれないといけなくてなんか冗長な感じになりました。

```PHP
// 文字列->塩基配列
function convertToDNA($text){
  $hex = bin2hex($text);
  $nucleotideArray = array("AA", "AT", "AC", "AG", "TA", "TT", "TC", "TG", "CA", "CT", "CC", "CG", "GA", "GT", "GC", "GG");
  $hexArray = array("/0/", "/1/", "/2/", "/3/", "/4/", "/5/", "/6/", "/7/", "/8/", "/9/", "/a/", "/b/", "/c/", "/d/", "/e/", "/f/");
  $result = preg_replace($hexArray, $nucleotideArray, $hex);
  return $result;
}

// 塩基配列->文字列
function convertToLanguage($text){
  $strArray = str_split($text, 2);
  $resultArray = array_map("dnaDecode", $strArray);
  $hex = implode("", $resultArray);
  return hex2bin($hex);
}

function dnaDecode($nucleotide){
  $nucleotideArray = array("/AA/", "/AT/", "/AC/", "/AG/", "/TA/", "/TT/", "/TC/", "/TG/", "/CA/", "/CT/", "/CC/", "/CG/", "/GA/", "/GT/", "/GC/", "/GG/");
  $hexArray = array("0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f");
  $result = preg_replace($nucleotideArray, $hexArray, $nucleotide);
  return $result;
}
```

### JavaScript(2020/01/31追加)
jsも長い感じになりましたがもっと縮小できるかも（var と let の使い分けもわかってないです...）

文字列⇔16進数の部分は下記を参考にしました。

[【JavaScript】文字列 ⇔ UTF8の16進数文字列変換](https://yasuhallabo.hatenadiary.org/entry/20140211/1392131668)

```js
var dnaHexValue = {'AA': '0', 'AT': '1', 'AC': '2', 'AG': '3',
         'TA': '4', 'TT': '5', 'TC': '6', 'TG': '7',
         'CA': '8', 'CT': '9', 'CC': 'a', 'CG': 'b',
         'GA': 'c', 'GT': 'd', 'GC': 'e', 'GG': 'f'};

function convertToDNA(text) {
  var hex = stringToHexString(text);
  var result = hex;
  for (var dna in dnaHexValue) {
    result = stringReplaceAll(result, dnaHexValue[dna], dna);
  }
  return result;
}

function convertToLanguage(text) {
  var dnaArray = splitStringInto(text, 2);
  var result = ""
  for (var index in dnaArray) {
    result = result + dnaHexValue[dnaArray[index]];
  }
  return hexStringToString(result);
}

function stringReplaceAll(text, target, replacement) {
  var regexp = new RegExp(target, 'g');
  return text.replace(regexp, replacement);
}

function splitStringInto(text, length) {
  var result = [];
  var index = 0;
  while (index + length <= text.length) {
    result.push(text.substring(index, index + length));
    index = index + length;
  }
  return result;
}

// 文字列を16進文字列に変換
function stringToHexString(text) {
	var bytes = stringToBytes(text);
	var hexString = bytesToHexString(bytes);
	return hexString;
}

// 16進文字列を文字列に変換
function hexStringToString(hexString) {
	var bytes = hexStringToBytes(hexString);
	var string = bytesToString(bytes);
	return string;
}

// 文字列をバイト配列に変換
function stringToBytes(text) {
    var result = [];
    if (text == null)
        return result;
    for (var i = 0; i < text.length; i++) {
        var c = text.charCodeAt(i);
        if (c <= 0x7f) {
            result.push(c);
        } else if (c <= 0x07ff) {
            result.push(((c >> 6) & 0x1F) | 0xC0);
            result.push((c & 0x3F) | 0x80);
        } else {
            result.push(((c >> 12) & 0x0F) | 0xE0);
            result.push(((c >> 6) & 0x3F) | 0x80);
            result.push((c & 0x3F) | 0x80);
        }
    }
    return result;
}

// バイト値を16進文字列に変換
function byteToHexString(byteNum) {
	var digits = (byteNum).toString(16);
  if (byteNum < 16) return '0' + digits;
  return digits;
}

// バイト配列を16進文字列に変換
function bytesToHexString(bytes) {
	var	result = "";
	for (var i = 0; i < bytes.length; i++) {
		result += byteToHexString(bytes[i]);
	}
	return result;
}

// 16進文字列をバイト値に変換
function hexStringToByte(hexString) {
	return parseInt(hexString, 16);
}

// バイト配列を16進文字列に変換
function hexStringToBytes(hexString) {
	var	result = [];
	for (var i = 0; i < hexString.length; i+=2) {
		result.push(hexStringToByte(hexString.substr(i,2)));
	}
	return result;
}

// バイト配列を文字列に変換
function bytesToString(bytes) {
    if (bytes == null)
        return null;
    var result = "";
    var i;
    while (i = bytes.shift()) {
        if (i <= 0x7f) {
            result += String.fromCharCode(i);
        } else if (i <= 0xdf) {
            var c = ((i&0x1f)<<6);
            c += bytes.shift()&0x3f;
            result += String.fromCharCode(c);
        } else if (i <= 0xe0) {
            var c = ((bytes.shift()&0x1f)<<6)|0x0800;
            c += bytes.shift()&0x3f;
            result += String.fromCharCode(c);
        } else {
            var c = ((i&0x0f)<<12);
            c += (bytes.shift()&0x3f)<<6;
            c += bytes.shift() & 0x3f;
            result += String.fromCharCode(c);
        }
    }
    return result;
}
```

# さいごに
変換後の塩基配列をどうにか圧縮したいのですが、そうすると圧縮した印が必要になったり...（[TATAボックス](https://ja.wikipedia.org/wiki/TATA%E3%83%9C%E3%83%83%E3%82%AF%E3%82%B9)でも付けるか:thinking:）

圧縮するにしてもswift, kotlin, PHPで方法は揃える必要があるし...悩みは尽きないです。