mirror of
https://github.com/fergalmoran/picard.git
synced 2025-12-25 19:04:09 +00:00
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:
125
picard/util/bytes2human.py
Normal file
125
picard/util/bytes2human.py
Normal 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
74
test/data/b2h_test_C.dat
Normal 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
|
||||
74
test/data/b2h_test_fr_FR.UTF-8.dat
Normal file
74
test/data/b2h_test_fr_FR.UTF-8.dat
Normal 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
69
test/po/fr.po
Normal 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
99
test/test_bytes2human.py
Normal 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
|
||||
Reference in New Issue
Block a user