【Swift】Tips: あると便利だったextension達(剰余算編)

はじめに

モバイルアプリサービス部の中安です。

アプリを作ってきた際に、あるとなかなか便利だったextensionをボチボチとご紹介していければと思っています。 アプリの目的に合わせてやりかえる必要があったり、もっとよい実装があるかもしれませんが、何かの役に立てば光栄です。

今回は剰余算編です。

すごく基本的なお話

小学校の算数問題になりますが、割り算をすると「商」と「余り(剰余)」が導き出されます。

被除数 ÷ 除数 = 商・・・剰余
被除数 = 商 × 除数 + 剰余

「被除数」と「除数」は、小学生の時は「わる数」と「わられる数」と教えられてました。懐かしいですね。

Swiftでは

Swiftにおいて商を求める際には / 演算子、剰余を求める際には % 演算子を使用します。

let a: Int = 60, b: Int = 7
a % b // = 4
a / b // = 8

上の 被除数 = 商 × 除数 + 剰余 の式に当てはめると

(a / b) * b + (a % b) // 60 => 60(被除数) = 8(商) * 7(除数) + 4(剰余)

ですね。

浮動小数点数の場合

さて、ここまでは整数のお話でした。

浮動小数点数を用いる場合は少し扱いが変わります。これを少し実践的な例を挙げて話をします。

例えば、幅が 350px の View に 幅が 44px の Subview を均等に配置するというUIがあるとします。 これを AutoLayout の制約を用いないでプログロマブルに実装することとします。

Subviewの数は?

この場合に Subview はいくつ配置できるでしょうか。簡単な話、単純に割って商を出せばいいわけです。

let viewWidth = 350, subviewWidth = 44
viewWidth / subviewWidth // 7

答えは7つです。

しかしながら、こういった座標やサイズなどは基本的に CGRect や CGSize などから、 CGFloat ベースで計算するはずです。

let viewWidth: CGFloat = 350, subviewWidth: CGFloat = 44
viewWidth / subviewWidth // 7.9545454

CGFloat の正体は Double 値ですから、整数と同じように計算しても導き出される計算の結果は浮動小数点数として扱われます。 しかし、この場合は個数を求めたいので期待値とは違います。

このような計算をしたい場合は、一旦 Int にキャストする方法なども考えられますが、以下の方法がベターかと思います。

let viewWidth: CGFloat = 350, subviewWidth: CGFloat = 44
(viewWidth / subviewWidth).rounded(.towardZero) // 7.0

rounded(_:) メソッドに .towardZero をパラメータとして渡してやることで、割り算をして求められた商をゼロに向かって丸めます。 すると、7.9545454 となっていた値は 7.0 へと丸められ、個数としての期待値に当てはまります。

Subview同士の間隔は?

ここまでで、 Subview の個数は求めることができました。では、何px間隔で Subview を配置すれば均等に置くことができるでしょうか。

これも小学生の時に算数でやった「植木算」を思い出すと簡単です。

7つのモノが置かれる場合、それらの間の数は (7-1) の 6 です。 View の幅を Subview の幅で割った余りを 6 で割れば View 同士の間隔になります。

let viewWidth: CGFloat = 350, subviewWidth: CGFloat = 44
viewWidth % subviewWidth // エラー!

整数の時と同じように上記のように実装するとエラーになります。 Swift では、Int 系の型には % 演算子が存在していますが、 浮動小数点数系には % 演算子が存在しません (存在していたが消された)。

代わりに浮動小数点数のオブジェクトには剰余を求めることのできるメソッドが用意されています。

let viewWidth: CGFloat = 350, subviewWidth: CGFloat = 44
viewWidth.truncatingRemainder(dividingBy: subviewWidth) // 42.0

実際に整数で剰余を求めた時と同じ値になったかと思います。

同様に remainder(dividingBy:) という剰余を求めるメソッドも容易されていますが、 何が違うかというと商の小数を四捨五入するとか切り捨てるかの違いがあります。 実際には 7.9545454という商を truncatingRemainder(dividingBy:) は小数を切り捨てた 7.0 として、 remainder(dividingBy:) は小数を四捨五入した 8.0 として扱った剰余が返されます。

実際に使用した例がこちらです。

let viewWidth: CGFloat = 350, subviewWidth: CGFloat = 44
viewWidth.truncatingRemainder(dividingBy: subviewWidth) // 42.0
viewWidth.remainder(dividingBy: subviewWidth) // -2.0

どちらの戻り値も逆算すれば被除数に戻せるのですが、 '%' is unavailable: Use truncatingRemainder instead というエラー文にあるように、 前者の truncatingRemainder(dividingBy: を使う方が推奨されているようです。

まとめ

ここまで書いたことをソースに起こしてみると下記のようになります。

let viewWidth: CGFloat = 350, subviewWidth: CGFloat = 44
let num = (viewWidth / subviewWidth).rounded(.towardZero) // 7.0
let remain = viewWidth.truncatingRemainder(dividingBy: subviewWidth) // 42.0
let interval = remain / (num - 1) // 7.0

これだけの数値が揃えば、求められる配置は実現できます。

extension化

さて、こういった計算が増えてくると、上項で書いたようなソースは煩わしくなってきます。 もう少しシンプルに、整数と同じように剰余を求められないかと思うわけです。 さらに言えば、今回の例はCGFloatだけに絞りましたが、他の浮動小数点数系の型にもそれらを実現させたいところです。

それには演算子をextensionとして取り付けてやる方法が便利でした。

そもそも、 前述の rounded(_:) は何処に定義されているメソッドなのかというと、 FloatingPointというプロトコルの中にあります。 そこで、この FloatingPoint に extension で新しく演算子を定義してやれば、Double 型でも Float 型でも汎用的に使用することができます。

extension FloatingPoint {
    
    static func %(lhs: Self, rhs: Self) -> Self {
        //return lhs.remainder(dividingBy: rhs)
        return lhs.truncatingRemainder(dividingBy: rhs)
    }
}

これで整数と同じように剰余を A % B の形で求めることができます。

もうひとつの「商」の方ですが、こちらは独自の演算子を作る方法をとりました。

truncatingRemainder(dividingBy:) もまた FloatingPoint に定義されているメソッドになります。

infix operator /%

extension FloatingPoint {
    
    static func /%(lhs: Self, rhs: Self) -> Self {
        return (lhs / rhs).rounded(.towardZero)
    }
}

独自の演算子は多用しすぎると可読性が落ちます。チーム開発など複数人の開発者がいる場合にはご注意いただきたいですが、 今回は「剰余を考慮した割り算」という意味で /% という演算子を独自定義しています。

// before
let viewWidth: CGFloat = 350, subviewWidth: CGFloat = 44
let num = (viewWidth / subviewWidth).rounded(.towardZero) // 7.0
let remain = viewWidth.truncatingRemainder(dividingBy: subviewWidth) // 42.0
let interval = remain / (num - 1) // 7.0

// after
let viewWidth: CGFloat = 350, subviewWidth: CGFloat = 44
let num = viewWidth /% subviewWidth // 7.0
let remain = viewWidth % subviewWidth // 42.0
let interval = remain / (num - 1) // 7.0

こうすることで算術から文章が取り払われて、すっきりしました。