summer_tree_home

Check iOでPython3をマスターするぜっ

Friendly number (Scientific Expedition) - 数値の書式指定

Scientific Expeditionに新しい問題が追加されていた。

どんな問題?

Friendly number
http://www.checkio.org/mission/friendly-number/

3,000→3k、50,000,000→50M のように、数字を、基数の累乗を使った表現に変換せよ。

  • 基数は、引数のbaseで指定した値を使う。
  • 係数の小数部は、decimalsで指定した桁数とする。
  • decimalsが0のときは小数部を切り捨て、それ以外は通常の丸め処理を行う。
  • powersで指定した累乗倍数を示す文字を追加する。(k,M,Gなど)
  • 接尾語(suffix)が指定されていれば追加する。

文章での説明より、例題を見た方がわかりやすい。

例題:

friendly_number(102) == '102'
friendly_number(10240) == '10k'
friendly_number(12341234, decimals=1) == '12.3M'
friendly_number(12000000, decimals=3) == '12.000M'
friendly_number(12461, decimals=1) == '12.5k'
friendly_number(1024000000, base=1024, suffix='iB') == '976MiB'
friendly_number(-150, base=100, powers=['', 'd', 'D']) == '-1d'
friendly_number(-155, base=100, decimals=1, powers=['', 'd', 'D']) == '-1.6d'
friendly_number(255000000000, powers=['', 'k', 'M']) == '255000M'

数値を人間が見やすい書式に変換する問題で、普段のプログラミングでも使えそうだ。
ちなみに、今回の問題に限って、checkio()関数ではなく、friendly_number()関数を作成することになる。

どうやって解く?

やること自体はそれほど難しくなさそう。

累乗の処理は、例えば base=1000 で、powers=['','k','M'] であれば、

  • 100万以上なら100万で割って'M'を追加
  • 1000以上なら1000で割って'k'を追加
  • 1000未満なら、1で割って''を追加。(つまり、そのまま)

というように大きい方からチェックしていけばいい。

小数部の処理は、str.format()を使えばいいのだが、

'{:.2f}'.format(n)

この2の部分を変数で指定するにはどうしたらいいのだろうか?そういう構文は無いのかな?
あれこれ考えて、

'{{:.{}f}}'.format(decimals).format(n)

とした。これで、decimals=2なら、

'{:.2f}'.format(n)

と同じ結果が得られる。
decimals==0 の場合は小数部を切り捨てなので、math.floor()を使えばいい。

まとめ

def friendly_number(number, base=1000, decimals=0, suffix='',
                    powers=['', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']):
    for power in range(len(powers) - 1, -1, -1):
        n = number / (base ** power)
        if n >= 1 or power == 0:
            s = str(floor(n)) if decimals == 0 else "{{:.{}f}}".format(decimals).format(n)
            return s + powers[power] + suffix

さあ、これで実行。と、エラー。

Your result: "-150"
Right result: "-1d"
Fail: friendly_number(-150, base=100, powers=["","d","D"])

おっと、numberが負の場合を考えてなかった。負数ならマイナス記号を最後に追加すればいいか。

まとめ2

def friendly_number(number, base=1000, decimals=0, suffix='',
                    powers=['', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']):
    sign = '-' if number < 0 else ''  # 負数ならマイナス記号を追加
    number = abs(number)
    for power in range(len(powers) - 1, -1, -1):
        n = number / (base ** power)
        if n >= 1 or power == 0:
            s = str(floor(n)) if decimals == 0 else "{{:.{}f}}".format(decimals).format(n)
            return sign + s + powers[power] + suffix

http://www.checkio.org/mission/friendly-number/publications/natsuki/python-3/first/
これで公開!

他の人の答え

今回、他の人の解答を見ていて、反省&感心する点が多かった・・・(いつもだけど)

1. 小数部の切り捨ては、int(n)でよかった。(Sim0000氏からご指摘いただいた。感謝!)

2. formatの{ }はネストできる!知らなかった。

"{:.{}f}".format(n, 2)  # "{:.2f}".format(n) と同じ

3. マイナス記号を追加するとき、math.copysign()を使う方法がある。

str.format()メソッドで { } はネストできる

format_spec フィールドは入れ子になった置換フィールドを含むこともできます。入れ子になった置換フィールドはフィールド名だけを含むことができます。変換フラグや書式指定は不可です。 format_spec 中の置換フィールドは format_spec 文字列が解釈される前に置き換えられます。これにより、値の書式を動的に指定することが可能になります。
http://docs.python.jp/3.3/library/string.html?#format-string-syntax

自分であれこれ試してみた。

>>> a, b = 3.141592, 2.718281
>>> '{:.{}f}'.format(a, 2)
'3.14'  # {:.2f}

>>> '{:{}}'.format(a, '.2f')
'3.14'  # {:.2f}

>>> '{:.{}f}-{:.{}f}'.format(a, 2, b, 3)  # 引数の順番に注意
'3.14-2.718'  # {:.2f}-{:.3f}

>>> '{:.{dec_a}f}-{:.{dec_b}f}'.format(a, b, dec_a=2, dec_b=3)  # フィールド名を指定
'3.14-2.718'  # {:.2f}-{:.3f}

フィールド名が無い場合は、「{」の登場順と引数の順番とが一致、ということでいいのかな。
入れ子の{ }には、フィールド名を指定した方がわかりやすいだろう。

math.copysign()メソッド

math.copysign(x,y)
x に y の符号を付けて返します。符号付きのゼロをサポートしているプラットフォームでは、 copysign(1.0, -0.0) は -1.0 を返します。
http://docs.python.jp/3.3/library/math.html?highlight=copysign#math.copysign

こちらも試してみる。

>>> math.copysign(3.14, -5.0)
-3.14
>>> math.copysign(-3.14, -5.0)
-3.14
>>> math.copysign(3.14, 5.0)
3.14
>>> math.copysign(-3.14, 5.0)
3.14

xの符号にかかわらず、yの符号が付くようだ。

「符号付きのゼロをサポートしているプラットフォームでは、 copysign(1.0, -0.0) は -1.0 を返します。」とあるので、試してみた。

>>> math.copysign(3.14, 0.0)
3.14
>>> math.copysign(3.14, -0.0)
-3.14

うちの Python3.3.5 (Win32) は「符号付きのゼロをサポートしているプラットフォーム」らしい。プラットフォームって具体的になんだろ?環境によって答えが違うのであれば怖いなぁ。

ちなみに、-0.0 と -0 では結果が違った。ややこしい。

>>> math.copysign(3.14, -0.0)
-3.14
>>> math.copysign(3.14, -0)
3.14

infやnanでも使える。

>>> math.copysign(3.14, float('inf'))
3.14
>>> math.copysign(3.14, float('-inf'))
-3.14
>>> math.copysign(3.14, float('nan'))
3.14
>>> math.copysign(3.14, float('-nan'))
-3.14

ところで、-nanってなんなんだ?(ダジャレではない)

再帰呼び出しを使った解答

すごいなぁと思ったのがこれ。

def friendly_number(number, base=1000, decimals=0, suffix='',
                    powers=['', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']):
    """
    Format a number as friendly text, using common suffixes.
    """   
    return '-' + friendly_number(-number, base, decimals, suffix, powers) if number < 0 else friendly_number(number / float(base), base, decimals, suffix, powers[1:]) if number >= base and len(powers) > 1 else ('%%.%df' % decimals if decimals else '%d') % number + powers[0] + suffix

http://www.checkio.org/mission/friendly-number/publications/lightholy/python-3/one-long-line/

if~elif~elseで書くとこうなる。

if number < 0:
    return '-' + friendly_number(-number, base, decimals, suffix, powers)
elif number >= base and len(powers) > 1:
    return friendly_number(number / float(base), base, decimals, suffix, powers[1:])
else:
    return ('%%.%df' % decimals if decimals else '%d') % number + powers[0] + suffix

number<baseとなるまで再帰を繰り返す。powersを1つずつ減らしていくのがポイント。
負数の処理もスマートで、numberの符号を取ってfriendly_number()に渡し、最後に'-'を追加する。

再帰って普段ほとんど使わないけど、うまく使えばとても美しいなぁ。