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()に渡し、最後に'-'を追加する。
再帰って普段ほとんど使わないけど、うまく使えばとても美しいなぁ。