5
3

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-01-12

はじめに

この記事で公開したアプリの中身についてです。

この記事(どこまでショボいアプリがAppleの審査に通るのか試してみた)をみてわりと機能が少なくてもアプリ公開できるのか!と思い正月休みにアプリをつくってみました。

以前作ったこれ(文字列→塩基配列の相互変換ツールをつくってみた(PHP))をアプリにしてリリースしました。

リリースしたアプリ

つくったアプリは有料です。(目指せ!!トータルダウンロード数25!!!)

  • Mac, iOS: ¥120
  • Android : ¥100

Macアプリ

ターゲット:MacOS Catalina以降

DNA変換

iOSアプリ

ターゲット:iOS13以降

DNA変換

Androidアプリ

ターゲット:Android6.0以降

DNA変換

Web版

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

http://adventam10.php.xdomain.jp/dna/index.php

2020/01/31追加

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

http://adventam10.html.xdomain.jp/dna/

アプリ概要

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

文字列->塩基配列 塩基配列->文字列
ios_dna_1 ios_dna_2

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 で公開してます。

方法

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

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

文字列->塩基配列

  1. 文字列 -> 2進数に変換
  2. 2進数 -> 16進数に変換
  3. 16進数 -> 塩基配列に変換(ATCG)

塩基配列->文字列

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

変換コード

もっといい方法があればぜひ教えて下さい!!

swift

swift では String の Extension で文字列から16進数への変換、16進数から2進数への変換、文字列を2文字ずつ分割する変数とメソッドをつくりました。(swift が一番めんどくさい感じになってしまいました...:cry:

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: ",")
    }
}
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が一番スッキリした感じにかけました。

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は変換のときにバックスラッシュいれないといけなくてなんか冗長な感じになりました。

// 文字列->塩基配列
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進数文字列変換

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ボックスでも付けるか:thinking:

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

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3