diff --git a/picard/util/bytes2human.py b/picard/util/bytes2human.py new file mode 100644 index 000000000..7bf1011ad --- /dev/null +++ b/picard/util/bytes2human.py @@ -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() diff --git a/test/data/b2h_test_C.dat b/test/data/b2h_test_C.dat new file mode 100644 index 000000000..5693d8825 --- /dev/null +++ b/test/data/b2h_test_C.dat @@ -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 diff --git a/test/data/b2h_test_fr_FR.UTF-8.dat b/test/data/b2h_test_fr_FR.UTF-8.dat new file mode 100644 index 000000000..f781da84e --- /dev/null +++ b/test/data/b2h_test_fr_FR.UTF-8.dat @@ -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 diff --git a/test/po/fr.po b/test/po/fr.po new file mode 100644 index 000000000..1f58b585b --- /dev/null +++ b/test/po/fr.po @@ -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 \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" diff --git a/test/test_bytes2human.py b/test/test_bytes2human.py new file mode 100644 index 000000000..06038bb41 --- /dev/null +++ b/test/test_bytes2human.py @@ -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