マルエツのレシート応募キャンペーンの最大口数を知りたい

スーパーのマルエツのキャンペーンでは、レシート3000円で1口応募できる。(レシート合算可) https://www.ichance.jp/cp/maruetsu-dreamwinter/

たとえば

以下のレシートがあった場合は

  • 600
  • 1000
  • 2000
  • 2500

以下のようにまとめることで「2口応募」できる。

  • 1口目 600, 2500
  • 2口目 1000, 2000

以下のレシートがあった場合はどうだろう。

  • 1676
  • 843
  • 1000
  • 233
  • 1731
  • 321
  • 179
  • 259
  • 460
  • 1594
  • 3544
  • 632
  • 2172
  • 2623
  • 1102
  • 84
  • 466
  • 306
  • 855
  • 3255
  • 1609
  • 1345
  • 1170
  • 4220
  • 6231
  • 180
  • 91
  • 2937
  • 544
  • 1577
  • 421
  • 3899
  • 585
  • 671
  • 1811
  • 875
  • 384
  • 950
  • 1618

これは、パッとわからない。
パッとわからないので、テキトーに組み合わせて出来た結果が「10口」だとする。
もうすこし組み合わせを頑張れば「11口」や「12口」になったりするかもしれない。
しかし、テキトーに手作業だとだいぶ疲れるし、無意味なような気がするので、以下のアルゴリズムを考えた。


アルゴリズム

計算1

  1. レシートから1枚ランダムに選んで、テーブルに置く
  2. テーブルの上にあるレシートを見る
    1. レシートの合計が3000円以上なら、コップを用意してそこにテーブルの上のレシートを入れる。1. を実行する
    2. レシートの合計が3000円未満なら、1. を実行する
    1. と 2. をレシートを取り尽くすまで実行する。
    2. レシートを取り尽くしたら、出来たコップ(3000円以上のレシートが入ったコップ)の数を記録する

計算1をN回繰り返し、出来たコップの数の記録を見る。
最も多い「コップの数」が、乱択の結果、確率的に分かった応募できる最大の口数。(=必ずしも正解とは言えない)


上のコードを swift で書いた。(Nは100万回)

import Foundation

let passValue: Int = 3000       // 基準値
let tryMax: Int = 1000000       // 試行回数

// 入力
let inputValues: [Int] = [
    1676,
    843,
    1000,
    233,
    1731,
    321,
    179,
    259,
    460,
    1594,
    3544,
    632,
    2172,
    2623,
    1102,
    84,
    466,
    306,
    855,
    3255,
    1609,
    1345,
    1170,
    4220,
    6231,
    180,
    91,
    2937,
    544,
    1577,
    421,
    3899,
    585,
    671,
    1811,
    875,
    384,
    950,
    1618
]


// あらかじめ基準値以上のグループを作成する
var passedGroups: [[Int]] = []          // すでに基準値以上のグループ
var inputValues2: [Int] = []

for value in inputValues {
    if value >= passValue {
        passedGroups.append([value])
        continue
    }
    
    inputValues2.append(value)
}

var groupNumMap: [Int: Int] = [:]     // グループ数の出現頻度のマップ
var bestGroups: [[Int]] = []          // 最適なグループ配列
var lastPercent: Int = 0

// print("- count \(inputValues2.count)")
// print("- sum \(inputValues2.reduce(0, +))")
// print("")

// ランダムに基準値を超えるグループを作成する
for i in 0..<tryMax {
    // 一時変数を準備する
    var tempGroups: [[Int]] = []    // 一時的なグループの配列
    var tempValues: [Int] = inputValues2  // 一時的な値の配列
    var tempGroup: [Int] = []       // 一時的なグループ
    
    // ランダムに基準値以上のグループの配列を作成する
    while true {
        // ランダムなインデックスを得る
        let index: Int = Int.random(in: 0..<tempValues.count)
        let value: Int = tempValues[index]
        tempValues.remove(at: index)
        
        // 一時的なグループに値を追加する
        tempGroup.append(value)
        let reduceValue: Int = tempGroup.reduce(0, +)
        
        if reduceValue >= passValue {
            // 一時的なグループの合計が基準値以上の場合
            // -> グループを、一時的なグループ配列に追加する
            tempGroups.append(tempGroup)
            tempGroup = []  // 一時的なグループをクリア
        }
        
        if tempValues.count > 0 {
            continue
        }

        // 値がなくなったので試行終了
        break
    }
    
    // グループ数の出現カウントを控える
    let groupsCount: Int = tempGroups.count + passedGroups.count
    groupNumMap[groupsCount] = (groupNumMap[groupsCount] ?? 0) + 1
    
    if tempGroups.count > bestGroups.count {
        // 最適なグループ数を超えたら、最適なグループを更新する
        bestGroups = tempGroups
    }
    
    let percent: Int = Int((Float(i) / Float(tryMax)) * 100)
    if percent > lastPercent {
        lastPercent = percent
        print("\(percent)%")
    }
}

// 計算の結果得た最適なグループ配列に、あらかじめ作成したすでに基準値以上のグループを合成する
bestGroups.insert(contentsOf: passedGroups, at: 0)

print("\n\n")

// 結果を出力
print("# Result\n")

// 最適なグループ配列を出力
print("## Best groups")
print("group numbers : \(bestGroups.count)")
print("")

for i in 0..<bestGroups.count {
    print("### Group\(i + 1)")
    for j in 0..<bestGroups[i].count {
        print("- \(bestGroups[i][j])")
    }
    
    print("")
}

// グループ数の出現頻度
print("## Frequency of group numbers\n")
let sortedKeys: [Int] = groupNumMap.keys.sorted()
for key in sortedKeys {
    print("### \(key)")
    print("- count \(groupNumMap[key] ?? 0)")
    
    let percent: Float = (Float(groupNumMap[key] ?? 0) / Float(tryMax)) * 100
    print("- percent \(percent)%")
}

これを実行すると、結果の出力は以下。
15口応募できるようだ。

全体のうち、口数の出現頻度が

  • 12口が 0.7%
  • 13口が 45%
  • 14口が 52%
  • 15口が 1%

となる。

# Result

## Best groups
group numbers : 15

### Group1
- 3544

### Group2
- 3255

### Group3
- 4220

### Group4
- 6231

### Group5
- 3899

### Group6
- 2623
- 1102

### Group7
- 306
- 855
- 259
- 1609

### Group8
- 233
- 1594
- 544
- 180
- 632

### Group9
- 1676
- 1345

### Group10
- 466
- 2937

### Group11
- 950
- 460
- 321
- 1731

### Group12
- 1577
- 1000
- 585

### Group13
- 84
- 1170
- 2172

### Group14
- 91
- 1618
- 384
- 671
- 843

### Group15
- 179
- 1811
- 421
- 875

## Frequency of group numbers

### 12
- count 7584
- percent 0.7584%
### 13
- count 453582
- percent 45.3582%
### 14
- count 528201
- percent 52.8201%
### 15
- count 10633
- percent 1.0633%

青森県のオープンデータを使った「公衆 Wi-Fi スポット」を探せる iOS アプリを作った

青森県のオープンデータは「青い森オープンデータカタログ」にある。

オープンデータで作成されたものはサイト内の「アプリマーケット」というところに登録できるが、みた感じスマホアプリがなく、

アプリマーケット - 青い森オープンデータカタログAoi Mori Open Data Catalog

「活用事例」を見ても個人でスマホアプリ作っている感じがなかったので、今回試しに作ってみた。

活用事例 - 青い森オープンデータカタログAoi Mori Open Data Catalog

アプリの元になったデータはこの「青森県公衆無線LANアクセスポイント一覧」

青森県公衆無線LANアクセスポイント一覧 - 青い森オープンデータカタログAoi Mori Open Data Catalog

データは CSV であるが、実際にはこれをそのままアプリで使うのではなく、アプリで使いやすいように Python で加工して、json ファイルにしたものをアプリで使用。

たとえば、まったく同じ内容の施設が重複していたりするので、それをひとつにしたりとか、ひとつの施設に複数の Wi-Fi スポットがあったりするのでまとめたりとか。。

そんな感じで作りましたよ。

青森県 公衆Wi-Fiマップ

青森県 公衆Wi-Fiマップ

  • Daisuke Tonosaki
  • Navigation
  • Free

あ、、青森県が出身地です。


今回 iOS 15 以降がターゲットのアプリにしたのだけど

iOS 15 だと属性付き文字列を NSAttributedString ではなく新しい AttributedString を使って作れるので、以前より直感的に作れる気がする。

AttributedString

たとえばこんな表示をしたければ、AttributedString はこう作る。

f:id:daisuke-t-jp:20211018210051p:plain

var text1 = AttributedString("これは赤く小さい ")
text1.foregroundColor = .red
text1.font = .systemFont(ofSize: 16)

var text2 = AttributedString("これは青く大きい ")
text2.foregroundColor = .blue
text2.font = .boldSystemFont(ofSize: 20)
        
label.attributedText = NSAttributedString(text1 + text2)

iOS の Google AdMob のテスト広告が出なくなってしまった時の対応

ここに書いてある「デモ広告」のユニット ID を使って、テスト広告を表示させていたのだが、いつの間にか出なくなっていた。

実行時にこんなログが出る。

<Google> Cannot find an ad network adapter with the name(s): com.google.DummyAdapter. Remember to link all required ad network adapters and SDKs, and set -ObjC in the 'Other Linker Flags' setting of your build target.

ドキュメントをよく見たら

注意:アプリで app-ads.txt ファイルを設定している場合は、デモ広告ユニットを使って広告を読み込むために、次の行を app-ads.txt ファイルに含める必要があります。

と記載があったので、 app-ads.txt にコードを追加する。

... 既存の記載 ...
google.com, pub-3940256099942544, DIRECT, f08c47fec0942fa0

そうして数時間経ったら、前のように「デモ広告」が表示されるようになった。 以前はドキュメントにデモ用の app-ads.txt の記載あったかなぁ・・・

ちなみに、↑とは別に「テストデバイス」設定をする方法でも、デモ広告は表示される。

SwiftUI で下から出てくる Picker を作ってみる

UIKit の場合だと UITextField の inputView に UIPicker を設定して、UITextField をタッチすると下からニュッと Picker が出てくる。ができる。

それを SwiftUI でやろうとすると、適当なものが用意されていなかったので、作ってみた。

こんな感じ。

コードは GitHub にあります。

Xcode プロジェクトにローカルの SwiftPackage を追加する

SwiftPackageManager で配布されているパッケージを、ローカルに持ってきてそれを Xcode プロジェクトで参照して使用するメモ。

  1. パッケージのリポジトリを clone する。
  2. 対象のアプリのプロジェクトを開く
  3. Xcode のプロジェクトツリーに、パッケージのフォルダ(Package.swift があるフォルダ)をドラッグ&ドロップする。
  4. ターゲットの Link Binary With Libraries を開く
    • 追加の + ボタンをクリックする。
      • f:id:daisuke-t-jp:20210427214547p:plain:h500
    • 対象の Package を選び、追加する。
      • f:id:daisuke-t-jp:20210427214838p:plain:h500
  5. import できるようになっているので、ビルドする。

ローカルで参照したこの状態では Xcode 上でパッケージのコード編集してビルドすると、変更したパッケージの動作を試せるので、パッケージの挙動を試したり、修正することができる。

写真に写っている人・動物をヒエログリフにする iOS アプリを作ってみた

機械学習フレームワークを使ったアプリを作ってみたくなり、試しに作ってみた。ついでに最近、エジプトのヒエログリフが面白いなあ、と思っていたのでそれをアプリのテーマにした。

Egyptian Hieroglyphs Photo

Egyptian Hieroglyphs Photo

  • Daisuke Tonosaki
  • 写真/ビデオ
  • 無料

Apple の review に提出する際に、かなり機能が単純なアプリだからリジェクトされるかと思ったが、大丈夫だった。
(審査にデモビデオは添付した)

アプリを使って変換すると、こんな感じで写真 → エジプトのヒエログリフの壁画風の画像になる。

f:id:daisuke-t-jp:20210303213230p:plain:w500

f:id:daisuke-t-jp:20210303213422p:plain:h400 f:id:daisuke-t-jp:20210303213440p:plain:h400

アプリの動作はこんな感じ。

www.youtube.com

精度はそんなに良くなく、たとえば写真全体に人や動物が大きく写っていると、残念ながらうまくいかない。。


ざっくりとした処理の流れは↓の感じ。

  1. あらかじめ、使用するヒエログリフの画像を作成して用意しておく
  2. 写真から人や動物を認識する
  3. 認識した領域にもっとも特徴が近いヒエログリフを探し、該当するヒエログリフで写真を置き換える

やってみて悩ましかった点は

多数の画像(ヒエログリフ)から VNFeaturePrintObservation を作ると時間がかかりすぎる。アプリの利用上の障害がある、という問題があった。

解決策としては

  • 実行時に特徴検出を逐次作成するのではなく
  • あらかじめ、各画像の VNFeaturePrintObservation を作成しておいてそれを CoreData で DB に保存しておく
    • そしてそれをアプリにバンドルしておく
  • アプリの実行時には作っておいた DB からVNFeaturePrintObservation を fetch する

という形にした。

VNFeaturePrintObservation の継承元の VNObservationNSSecureCoding を採用しているので、CoreData の Attribute の型を Transformable にして、下のようなカスタムの Transformer クラスを使用することで CoreData に保存できる。

@objc(VNFeaturePrintObservationTransformer)
class VNFeaturePrintObservationTransformer: NSSecureUnarchiveFromDataTransformer {
    static let name = NSValueTransformerName(rawValue: String(describing: VNFeaturePrintObservationTransformer.self))
    
    override class var allowedTopLevelClasses: [AnyClass] {
        return super.allowedTopLevelClasses + [VNFeaturePrintObservation.self]
    }
    
    public static func register() {
        let transformer = VNFeaturePrintObservationTransformer()
        ValueTransformer.setValueTransformer(transformer, forName: name)
    }
}

Google ML Kit や Core ML はほとんど触ったことがなく難しい印象だったが、やってみると楽しかった。

iOS の Google MLKit で静止画像の ObjectDetection を試してみた

Google MLKit の ObjectDetection での静止画像の解析を試してみた。

公式のサンプルもあるが、バンドルされている静止画像しか解析できない。

そのため、写真を選んで解析する iOS サンプル を作ってみた。

動かすとこんな感じ。

試してみて分かったこと

  • Firebase のセットアップをしなくても使える
    • Google MLKit はもともとは Firebase MLKit という名前だったが
    • Firebase ではなくなり、Firebase のセットアップ(GoogleService-Info.plist の追加など)をしなくても最低限使えるようになったため、とっつきやすい。
  • 認識できるオブジェクトの最大数は 5 である
  • TensorFlowLite と比べると
    • TensorFlowLite ではモデルの用意が必要になるが、Google MLKit ではモデルを用意しなくても、標準でバンドルされているベースモデルを使ってくれるため、とりあえず簡単に解析をしてみたい場合は、Google MLKit を使うと良い。
  • ベースモデル以外にカスタムモデルも使用可能
    • カスタマイズされたモデルを使うことも可能なので、拡張性もある。
    • カスタマイズモデルの取り込み方法は、以下がある。
      • ローカルファイルから読み込む
      • Firebase でホスティングされているモデルを読み込む