浮動小数点数の剰余演算

概要:整数だけでなく、浮動小数点数にも対応した剰余演算を行う関数を作ります。

剰余演算を行うモジュロ(modulo)演算子 % は float 型や double 型には適用できません。
マジ!?……って言うか何で?
と、私もあっけにとられたものですが、
まあ、それくらい直ぐに自分でも作れるだろうと
浮動小数点数にも対応した剰余演算を行う処理を考えてみたものの、
これが意外と長いプログラムになりそうだと関数に仕様を改め、
考えること半時間……直ぐにはひらめかなかった自分が悲しかったりして……完成!
意外に長いプログラムと言っても10行程度なのですが、
ちょっとした問題としてはなかなか良い問題と思いここに紹介させて頂きます。
授業の課題なんかにいかが?

■詳細仕様

まず何をしたいかをハッキリさせましょう。
数式で表すと以下のようになります。

359%360=359
360%360=0
361%360=1
-359%360=1
-360%360=0
-361%360=359
0%360=0
-0%360=0
-1%360=359
360.123%360=0.123
-360.123%360=359.877


これは360度の円を思い描いてもらえば直ぐにわかるかと思います。
-359度は1度のことですね。
同様に-1度は359度のことですね。

また、割る数に負数は考えないことにします。
数学的にも理解できませんよね?

そして、計算結果……つまり、剰余にも負数は考えないことにします。
イメージするのは円!これが重要です。

■ % 演算子

ちなみに % 演算子の負数処理はどうなっているかというと、処理系に依るらしいです。
私の処理系では以下のようになりました。

-1%360=-1
-361%360=-1
1%-360=1
361%-360=1
-1%-360=-1
-361%-360=-1

■ちょっと待った!

たぶんこの問題は人に説明されるとすんなりわかっちゃいます。
以降の解説を読む前に、少し自分で考えてみましょう。

■関数原型

浮動小数点数に対応した剰余演算関数が目的ですから
引数、返却値のどちらも浮動小数点数でなければなりません。
したがって、float 型と double 型が考えられますが、
より汎用性の高い double 型にしましょう。

私が考えた関数の関数原型は以下の通りです。

関数の名前:modulo
割られる数:left
割る数:right

double modulo(double left,double right);

■ unsigned double 型?

割る数と返却値に負数は考えない事にしました。
ならば、それを強調するために、
そして、不正な値を渡さないように double 型に unsigned 修飾子を付けて
unsigned double と宣言した方が良いような気がしますよね?
しかし、unsigned double という型はありません。
したがって、関数内部で不正な引数が渡された時の処理をする必要があると言えます。

■割る数に負数が渡された場合

割る数に負数が渡された場合はその絶対値を使って計算することにしましょう。

if(right<0) right=-right;

■割られる数が 0 の場合

剰余は 0 なので、
そのまま割られる数を返しましょう。

return left;

■割られる数が正数の場合

普通の場合です。

さて、剰余とは割り算の余りの事ですね。
割り算は引き算を繰り返すことでも表現できます。
具体的には「割られる数−割る数」を繰り返していくことになります。
どんどん引いて、割られる数が割る数よりも小さくなった時の割られる数が余りです。
したがって、ループを実行する条件は「割られる数>=割る数」ですね。

最初から「割られる数<割る数」という可能性も考えられるので、
ループに入る前に条件判定をする while ループを使いましょう。

if(left>0){
  while(left-right>=0){
    left-=right;
  }
}
return left;


■割られる数が負数の場合

今回の肝であり、私がちょっと悩んだところです(ほ、ホントちょっとですって!)。

もう一度、最初に示した数式を見てみましょう。
何か気が付きませんか?

-359%360=1
-360%360=0
-361%360=359


どうやら「割られる数+割る数」を繰り返していき、
初めて正数になった時の値が求めたい計算結果ですね。
ここで、割る数は必ず正数であることに注意して下さい。
例え負数を渡されても正数に直してしまうんでしたね。

剰余は必ず正数であるとしましたから、
割られる数が負数の場合は必ず一度は加算を行うことになります。
したがって、ループの後に条件判定を行う do 〜 while ループを使いましょう。
もう一度ループに入る条件は「割られる数<0」です。
0も正しい剰余なので、0未満であることに注意して下さい。

if(left<0){
  do{
    left+=right;
  }while(left<0);
}
return left;


また、割られる数が正数の場合と負数の場合は絶対に両立しませんから
これらは if 〜 else if 文で繋げるべきです。

■関数としてまとめる

今までの処理を関数にまとめると以下のようになります。

double modulo(double left,double right)
{
    if(right<0) right=-right;

    if(left>0){
        while(left-right>=0){
            left-=right;
        }
    }else if(left<0){
        do{
            left+=right;
        }while(left<0);
    }
    return left;
}
割られる数が 0 の場合は、全く処理せずにそのまま返していますね。

他にも様々な実現方法が考えられるので、
各自オリジナルのアルゴリズムを考えてみて下さい。

■まとめ

私は意味もなく加減算を行ったわけではありません。
加減算は誤差が出ない」という重要な性質を考慮してのことです。
剰余演算という基本演算で誤差が出るのはマズイですからね。

そして、本文中でも述べましたが、
割り算は引き算の繰り返しである」も重要です。

初心忘るべからず、ですね。

■割る数が浮動小数点数の場合

ところで、割る数が浮動小数点数の場合には全く触れてきませんでしたが、
プログラムは正しく動きます。

例えば2πを周期とする弧度法で円を考えてみましょう。
この時「-π/2、-π、-3π/2」はそれぞれ
「3π/2、π、π/2」の事です。
もちろん今回作った modulo 関数で計算しても以下のように
正しい計算結果が得られます。

-1.57%6.28=4.71
-3.14%6.28=3.14
-4.71%6.28=1.57

■最後に

こんな感じで人に説明されればすんなりわかっちゃうかも知れませんが、
なかなか良い問題だと思いませんか?か?
是非お友達等に出題してみて下さい。
学生さんにオススメ!

戻る / ホーム