Add functions to convert bytes to human readable form.

Binary and decimal modes are supported (MB and MiB ie.)
It supports i18n using gettext and locale.
Precision can be modified if needed, by default it is using 1 digit (if needed).

Extensive tests were written, the toughest was to make them work for
both default C locale and fr_FR.UTF-8 locale (ofc it is possible to test for more
locales...). If one locale isn't available on testing system, test is skipped.
fr locale was chosen because decimal point is replaced by a comma and byte units
becomes "octet" units (1.5 MB in english -> 1,5 Mo in french).
This commit is contained in:
Laurent Monin
2013-05-27 12:50:16 +02:00
parent 429242f1c8
commit 6de59d8ee9
5 changed files with 441 additions and 0 deletions

125
picard/util/bytes2human.py Normal file
View File

@@ -0,0 +1,125 @@
# -*- coding: utf-8 -*-
#
# Picard, the next-generation MusicBrainz tagger
# Copyright (C) 2013 Laurent Monin
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import locale
import picard.i18n
"""
Helper class to convert bytes to human-readable form
It supports i18n through gettext, decimal and binary units.
>>> n = 1572864
>>> [binary(n), decimal(n)]
['1.5 MiB', '1.6 MB']
"""
#used to force gettextization
_BYTES_STRINGS_I18N = (
N_('%s B'),
N_('%s kB'),
N_('%s KiB'),
N_('%s MB'),
N_('%s MiB'),
N_('%s GB'),
N_('%s GiB'),
N_('%s TB'),
N_('%s TiB'),
N_('%s PB'),
N_('%s PiB'),
)
def decimal(number, prec=1):
"""
Convert bytes to short human-readable string, decimal mode
>>> [decimal(n) for n in [1000, 1024, 15500]]
['1 kB', '1 kB', '15.5 kB']
"""
return short_string(int(number), 1000)
def binary(number, prec=1):
"""
Convert bytes to short human-readable string, binary mode
>>> [binary(n) for n in [1000, 1024, 15500]]
['1000 B', '1 KiB', '15.1 KiB']
"""
return short_string(int(number), 1024, prec)
def short_string(number, multiple, prec=1):
"""
Returns short human-readable string for `number` bytes
>>> [short_string(n, 1024, 2) for n in [1000, 1100, 15500]]
['1000 B', '1.07 KiB', '15.14 KiB']
>>> [short_string(n, 1000, 1) for n in [10000, 11000, 1550000]]
['10 kB', '11 kB', '1.6 MB']
"""
num, unit = calc_unit(number, multiple)
n = int(num)
nr = round(num, prec)
if n == nr or unit == 'B':
fmt = '%d'
num = n
else:
fmt = '%%0.%df' % prec
num = nr
fmtnum = locale.format(fmt, num)
return _("%s " + unit) % fmtnum
def calc_unit(number, multiple=1000):
"""
Calculate rounded number of multiple * bytes, finding best unit
>>> calc_unit(12456, 1024)
(12.1640625, 'KiB')
>>> calc_unit(-12456, 1000)
(-12.456, 'kB')
>>> calc_unit(0, 1001)
Traceback (most recent call last):
...
ValueError: multiple parameter has to be 1000 or 1024
"""
if number < 0:
sign = -1
number = -number
else:
sign = 1
n = float(number)
if multiple == 1000:
k, b = 'k', 'B'
elif multiple == 1024:
k, b = 'K', 'iB'
else:
raise ValueError('multiple parameter has to be 1000 or 1024')
suffixes = ["B"] + [i + b for i in k + "MGTP"]
for suffix in suffixes:
if n < multiple or suffix == suffixes[-1]:
return (sign*n, suffix)
else:
n /= multiple
if __name__ == "__main__":
import doctest
doctest.testmod()

74
test/data/b2h_test_C.dat Normal file
View File

@@ -0,0 +1,74 @@
0;0 B;0 B;0 B
1;1 B;1 B;1 B
100;100 B;100 B;100 B
102;102 B;102 B;102 B
500;500 B;500 B;500 B
512;512 B;512 B;512 B
990;990 B;990 B;990 B
999;999 B;999 B;999 B
1000;1 kB;1000 B;1000 B
1013;1 kB;1013 B;1013 B
1023;1 kB;1023 B;1023 B
1024;1 kB;1 KiB;1 KiB
1500;1.5 kB;1.5 KiB;1.46 KiB
1536;1.5 kB;1.5 KiB;1.50 KiB
100000;100 kB;97.7 KiB;97.66 KiB
104857;104.9 kB;102.4 KiB;102.40 KiB
500000;500 kB;488.3 KiB;488.28 KiB
524288;524.3 kB;512 KiB;512 KiB
990000;990 kB;966.8 KiB;966.80 KiB
999900;999.9 kB;976.5 KiB;976.46 KiB
1000000;1 MB;976.6 KiB;976.56 KiB
1038090;1 MB;1013.8 KiB;1013.76 KiB
1048471;1 MB;1023.9 KiB;1023.90 KiB
1048576;1 MB;1 MiB;1 MiB
1500000;1.5 MB;1.4 MiB;1.43 MiB
1572864;1.6 MB;1.5 MiB;1.50 MiB
100000000;100 MB;95.4 MiB;95.37 MiB
107374182;107.4 MB;102.4 MiB;102.40 MiB
500000000;500 MB;476.8 MiB;476.84 MiB
536870912;536.9 MB;512 MiB;512 MiB
990000000;990 MB;944.1 MiB;944.14 MiB
999900000;999.9 MB;953.6 MiB;953.58 MiB
1000000000;1 GB;953.7 MiB;953.67 MiB
1063004405;1.1 GB;1013.8 MiB;1013.76 MiB
1073634449;1.1 GB;1023.9 MiB;1023.90 MiB
1073741824;1.1 GB;1 GiB;1 GiB
1500000000;1.5 GB;1.4 GiB;1.40 GiB
1610612736;1.6 GB;1.5 GiB;1.50 GiB
100000000000;100 GB;93.1 GiB;93.13 GiB
109951162777;110.0 GB;102.4 GiB;102.40 GiB
500000000000;500 GB;465.7 GiB;465.66 GiB
549755813888;549.8 GB;512 GiB;512 GiB
990000000000;990 GB;922 GiB;922.01 GiB
999900000000;999.9 GB;931.2 GiB;931.23 GiB
1000000000000;1 TB;931.3 GiB;931.32 GiB
1088516511498;1.1 TB;1013.8 GiB;1013.76 GiB
1099401676613;1.1 TB;1023.9 GiB;1023.90 GiB
1099511627776;1.1 TB;1 TiB;1 TiB
1500000000000;1.5 TB;1.4 TiB;1.36 TiB
1649267441664;1.6 TB;1.5 TiB;1.50 TiB
100000000000000;100 TB;90.9 TiB;90.95 TiB
112589990684262;112.6 TB;102.4 TiB;102.40 TiB
500000000000000;500 TB;454.7 TiB;454.75 TiB
562949953421312;562.9 TB;512 TiB;512 TiB
990000000000000;990 TB;900.4 TiB;900.40 TiB
999900000000000;999.9 TB;909.4 TiB;909.40 TiB
1000000000000000;1 PB;909.5 TiB;909.49 TiB
1114640907774197;1.1 PB;1013.8 TiB;1013.76 TiB
1125787316851939;1.1 PB;1023.9 TiB;1023.90 TiB
1125899906842624;1.1 PB;1 PiB;1 PiB
1500000000000000;1.5 PB;1.3 PiB;1.33 PiB
1688849860263936;1.7 PB;1.5 PiB;1.50 PiB
100000000000000000;100 PB;88.8 PiB;88.82 PiB
115292150460684704;115.3 PB;102.4 PiB;102.40 PiB
500000000000000000;500 PB;444.1 PiB;444.09 PiB
576460752303423488;576.5 PB;512 PiB;512 PiB
990000000000000000;990 PB;879.3 PiB;879.30 PiB
999900000000000000;999.9 PB;888.1 PiB;888.09 PiB
1000000000000000000;1000 PB;888.2 PiB;888.18 PiB
1141392289560778496;1141.4 PB;1013.8 PiB;1013.76 PiB
1152806212456386304;1152.8 PB;1023.9 PiB;1023.90 PiB
1152921504606846976;1152.9 PB;1024 PiB;1024 PiB
1500000000000000000;1500 PB;1332.3 PiB;1332.27 PiB
1729382256910270464;1729.4 PB;1536 PiB;1536 PiB

View File

@@ -0,0 +1,74 @@
0;0 o;0 o;0 o
1;1 o;1 o;1 o
100;100 o;100 o;100 o
102;102 o;102 o;102 o
500;500 o;500 o;500 o
512;512 o;512 o;512 o
990;990 o;990 o;990 o
999;999 o;999 o;999 o
1000;1 ko;1000 o;1000 o
1013;1 ko;1013 o;1013 o
1023;1 ko;1023 o;1023 o
1024;1 ko;1 Kio;1 Kio
1500;1,5 ko;1,5 Kio;1,46 Kio
1536;1,5 ko;1,5 Kio;1,50 Kio
100000;100 ko;97,7 Kio;97,66 Kio
104857;104,9 ko;102,4 Kio;102,40 Kio
500000;500 ko;488,3 Kio;488,28 Kio
524288;524,3 ko;512 Kio;512 Kio
990000;990 ko;966,8 Kio;966,80 Kio
999900;999,9 ko;976,5 Kio;976,46 Kio
1000000;1 Mo;976,6 Kio;976,56 Kio
1038090;1 Mo;1013,8 Kio;1013,76 Kio
1048471;1 Mo;1023,9 Kio;1023,90 Kio
1048576;1 Mo;1 Mio;1 Mio
1500000;1,5 Mo;1,4 Mio;1,43 Mio
1572864;1,6 Mo;1,5 Mio;1,50 Mio
100000000;100 Mo;95,4 Mio;95,37 Mio
107374182;107,4 Mo;102,4 Mio;102,40 Mio
500000000;500 Mo;476,8 Mio;476,84 Mio
536870912;536,9 Mo;512 Mio;512 Mio
990000000;990 Mo;944,1 Mio;944,14 Mio
999900000;999,9 Mo;953,6 Mio;953,58 Mio
1000000000;1 Go;953,7 Mio;953,67 Mio
1063004405;1,1 Go;1013,8 Mio;1013,76 Mio
1073634449;1,1 Go;1023,9 Mio;1023,90 Mio
1073741824;1,1 Go;1 Gio;1 Gio
1500000000;1,5 Go;1,4 Gio;1,40 Gio
1610612736;1,6 Go;1,5 Gio;1,50 Gio
100000000000;100 Go;93,1 Gio;93,13 Gio
109951162777;110,0 Go;102,4 Gio;102,40 Gio
500000000000;500 Go;465,7 Gio;465,66 Gio
549755813888;549,8 Go;512 Gio;512 Gio
990000000000;990 Go;922 Gio;922,01 Gio
999900000000;999,9 Go;931,2 Gio;931,23 Gio
1000000000000;1 To;931,3 Gio;931,32 Gio
1088516511498;1,1 To;1013,8 Gio;1013,76 Gio
1099401676613;1,1 To;1023,9 Gio;1023,90 Gio
1099511627776;1,1 To;1 Tio;1 Tio
1500000000000;1,5 To;1,4 Tio;1,36 Tio
1649267441664;1,6 To;1,5 Tio;1,50 Tio
100000000000000;100 To;90,9 Tio;90,95 Tio
112589990684262;112,6 To;102,4 Tio;102,40 Tio
500000000000000;500 To;454,7 Tio;454,75 Tio
562949953421312;562,9 To;512 Tio;512 Tio
990000000000000;990 To;900,4 Tio;900,40 Tio
999900000000000;999,9 To;909,4 Tio;909,40 Tio
1000000000000000;1 Po;909,5 Tio;909,49 Tio
1114640907774197;1,1 Po;1013,8 Tio;1013,76 Tio
1125787316851939;1,1 Po;1023,9 Tio;1023,90 Tio
1125899906842624;1,1 Po;1 Pio;1 Pio
1500000000000000;1,5 Po;1,3 Pio;1,33 Pio
1688849860263936;1,7 Po;1,5 Pio;1,50 Pio
100000000000000000;100 Po;88,8 Pio;88,82 Pio
115292150460684704;115,3 Po;102,4 Pio;102,40 Pio
500000000000000000;500 Po;444,1 Pio;444,09 Pio
576460752303423488;576,5 Po;512 Pio;512 Pio
990000000000000000;990 Po;879,3 Pio;879,30 Pio
999900000000000000;999,9 Po;888,1 Pio;888,09 Pio
1000000000000000000;1000 Po;888,2 Pio;888,18 Pio
1141392289560778496;1141,4 Po;1013,8 Pio;1013,76 Pio
1152806212456386304;1152,8 Po;1023,9 Pio;1023,90 Pio
1152921504606846976;1152,9 Po;1024 Pio;1024 Pio
1500000000000000000;1500 Po;1332,3 Pio;1332,27 Pio
1729382256910270464;1729,4 Po;1536 Pio;1536 Pio

69
test/po/fr.po Normal file
View File

@@ -0,0 +1,69 @@
msgid ""
msgstr ""
"Project-Id-Version: MusicBrainz\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2013-01-20 13:23+0100\n"
"PO-Revision-Date: 2013-01-20 13:26+0100\n"
"Last-Translator: Laurent Monin <i18n@norz.org>\n"
"Language-Team: French (http://www.transifex.com/projects/p/musicbrainz/language/fr/)\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 0.9.6\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: picard/util/bytes2human.py:47
#, python-format
msgid "%s B"
msgstr "%s o"
#: picard/util/bytes2human.py:48
#, python-format
msgid "%s kB"
msgstr "%s ko"
#: picard/util/bytes2human.py:49
#, python-format
msgid "%s KiB"
msgstr "%s Kio"
#: picard/util/bytes2human.py:50
#, python-format
msgid "%s MB"
msgstr "%s Mo"
#: picard/util/bytes2human.py:51
#, python-format
msgid "%s MiB"
msgstr "%s Mio"
#: picard/util/bytes2human.py:52
#, python-format
msgid "%s GB"
msgstr "%s Go"
#: picard/util/bytes2human.py:53
#, python-format
msgid "%s GiB"
msgstr "%s Gio"
#: picard/util/bytes2human.py:54
#, python-format
msgid "%s TB"
msgstr "%s To"
#: picard/util/bytes2human.py:55
#, python-format
msgid "%s TiB"
msgstr "%s Tio"
#: picard/util/bytes2human.py:56
#, python-format
msgid "%s PB"
msgstr "%s Po"
#: picard/util/bytes2human.py:57
#, python-format
msgid "%s PiB"
msgstr "%s Pio"

99
test/test_bytes2human.py Normal file
View File

@@ -0,0 +1,99 @@
import locale
import os.path
import shutil
import subprocess
import sys
import tempfile
import unittest
from picard.i18n import setup_gettext
from picard.util import bytes2human
class Testbytes2human(unittest.TestCase):
def setUp(self):
# we are using temporary locales for tests
self.tmp_path = tempfile.mkdtemp().decode("utf-8")
if sys.hexversion >= 0x020700F0:
self.addCleanup(shutil.rmtree, self.tmp_path)
self.localedir = os.path.join(self.tmp_path, 'locale')
test_locales = [('picard', 'fr', 'test/po/fr.po')]
for domain, locale, po in test_locales:
path = os.path.join(self.localedir, locale, 'LC_MESSAGES')
os.makedirs(path)
mo = os.path.join(path, '%s.mo' % domain)
assert(subprocess.call(['msgfmt', '-o', mo, po]) == 0)
def tearDown(self):
if sys.hexversion < 0x020700F0:
shutil.rmtree(self.tmp_path)
def test_00(self):
# testing with default C locale, english
lang = 'C'
setup_gettext(self.localedir, lang)
self.run_test(lang)
self.assertEqual(bytes2human.binary(45682), '44.6 KiB')
self.assertEqual(bytes2human.binary(-45682), '-44.6 KiB')
self.assertEqual(bytes2human.decimal(45682), '45.7 kB')
self.assertEqual(bytes2human.decimal(9223372036854775807), '9223.4 PB')
self.assertEqual(bytes2human.decimal(123.6), '123 B')
self.assertRaises(ValueError, bytes2human.decimal, 'xxx')
self.assertRaises(ValueError, bytes2human.decimal, '123.6')
self.assertRaises(ValueError, bytes2human.binary, 'yyy')
self.assertRaises(ValueError, bytes2human.binary, '456yyy')
try:
bytes2human.decimal(u'123')
except Exception as e:
self.fail('Unexpected exception: %s' % e)
def test_05(self):
# testing with french locale and translation
# 1.5 MiB -> 1,5 Mio
lang = 'fr_FR.UTF-8'
setup_gettext(self.localedir, lang)
self.run_test(lang)
def run_test(self, lang = 'C', create_test_data=False):
"""
Compare data generated with sample files
Setting create_test_data to True will generated sample files
from code execution (developper-only, check carefully)
"""
filename = os.path.join('test', 'data', 'b2h_test_%s.dat' % lang)
testlist = self._create_testlist()
if create_test_data:
self._save_expected_to(filename, testlist)
expected = self._read_expected_from(filename)
#self.maxDiff = None
self.assertEqual(testlist, expected)
if create_test_data:
# be sure it is disabled
self.fail('!!! UNSET create_test_data mode !!! (%s)' % filename)
def _create_testlist(self):
values = [0, 1]
for n in [1000, 1024]:
p = 1
for e in range(0,6):
p *= n
for x in [0.1, 0.5, 0.99, 0.9999, 1, 1.5]:
values.append(int(p*x))
l = []
for x in sorted(values):
l.append(";".join([str(x), bytes2human.decimal(x),
bytes2human.binary(x),
bytes2human.short_string(x, 1024, 2)]))
return l
def _save_expected_to(self, path, a_list):
with open(path, 'wb') as f:
f.writelines([l + "\n" for l in a_list])
f.close()
def _read_expected_from(self, path):
with open(path, 'rb') as f:
lines = [l.rstrip("\n") for l in f.readlines()]
f.close()
return lines