Passed
Push — 2.x ( a4d7e0...003ec3 )
by Jordi
07:40
created

Alphanumber.__str__()   A

Complexity

Conditions 1

Size

Total Lines 2
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 2
rs 10
c 0
b 0
f 0
cc 1
nop 1
1
# -*- coding: utf-8 -*-
2
#
3
# This file is part of SENAITE.CORE.
4
#
5
# SENAITE.CORE is free software: you can redistribute it and/or modify it under
6
# the terms of the GNU General Public License as published by the Free Software
7
# Foundation, version 2.
8
#
9
# This program is distributed in the hope that it will be useful, but WITHOUT
10
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12
# details.
13
#
14
# You should have received a copy of the GNU General Public License along with
15
# this program; if not, write to the Free Software Foundation, Inc., 51
16
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
#
18
# Copyright 2018-2021 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
import re
22
23
from bika.lims import api
24
25
_marker = object()
26
ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
27
28
29
class Alphanumber(object):
30
    """Represents an alphanumeric number
31
    """
32
33
    def __init__(self, number=0, num_chars=3, num_digits=3, alphabet=ALPHABET):
34
        if num_chars < 1:
35
            raise ValueError("num_chars param is lower than 1")
36
        if num_digits < 1:
37
            raise ValueError("num_digits param is lower than 1")
38
39
        self.alphabet = alphabet
40
        self.num_chars = num_chars
41
        self.num_digits = num_digits
42
        self.number = to_decimal(number, alphabet=alphabet)
43
44
        if self.number < 0:
45
            # TODO Support for negative values?
46
            raise ValueError("number is lower than 0")
47
48
    @property
49
    def alpha_format(self):
50
        return '%sa%sd' % (self.num_chars, self.num_digits)
51
52
    def __int__(self):
53
        return self.number
54
55
    def __index__(self):
56
        return self.__int__()
57
58
    def __str__(self):
59
        return self.__format__(self.alpha_format)
60
61
    def __repr__(self):
62
        return self.__str__()
63
64
    def __add__(self, other):
65
        number = self.__int__() + int(other)
66
        return Alphanumber(number=number, num_chars=self.num_chars,
67
                           num_digits=self.num_digits, alphabet=self.alphabet)
68
69
    def __sub__(self, other):
70
        number = self.__int__() - int(other)
71
        return Alphanumber(number=number, num_chars=self.num_chars,
72
                           num_digits=self.num_digits, alphabet=self.alphabet)
73
74
    def __lt__(self, other):
75
        return self.__int__() < int(other)
76
77
    def __gt__(self, other):
78
        return self.__int__() > int(other)
79
80
    def __eq__(self, other):
81
        return self.__int__() == int(other)
82
83
    def __format__(self, format):
84
        if self.alpha_format != format:
85
            return to_alpha(self.number, format, self.alphabet).format(format)
86
87
        base_format = "{alpha:%s>%s}{number:0%sd}" % (self.alphabet[0],
88
                                                      self.num_chars,
89
                                                      self.num_digits)
90
        alpha, number = self.parts()
91
        values = dict(alpha=alpha, number=number)
92
        return base_format.format(**values)
93
94
    def format(self, format):
95
        return self.__format__(format)
96
97
    def parts(self):
98
        """Returns the alphanumeric parts (chars + digits) of this Alphanum
99
        """
100
        def get_alpha(alpha_index, alphabet):
101
            if alpha_index >= len(alphabet):
102
                lead = get_alpha(alpha_index / len(alphabet), alphabet)
103
                trail = alphabet[alpha_index % len(alphabet)]
104
                return "{}{}".format(lead, trail)
105
            return alphabet[alpha_index]
106
107
        max_digits = 10 ** self.num_digits - 1
108
        alpha_index = abs(self.number) / max_digits
109
        alpha_number = abs(self.number) % max_digits
110
        # Note the 1 digit leap e.g. AA99 + 1 == AB01 (not AB00)
111
        if not alpha_number and abs(self.number):
112
            alpha_number = max_digits
113
            alpha_index -= 1
114
115
        alpha = get_alpha(alpha_index, self.alphabet)
116
        if len(alpha) > self.num_chars:
117
            raise ValueError("Out of bounds. Requires {} chars, {} set"
118
                             .format(len(alpha), self.num_chars))
119
120
        return alpha, alpha_number
121
122
123
def to_alpha(number, format, alphabet=ALPHABET, default=_marker):
124
    """Returns an Alphanumber object that represents the number in accordance
125
    with the format specified.
126
    :param number: a number representation used to create the Alphanumber
127
    :param format: the format to use. eg. '2a3d' for 2 chars and 3 digits
128
    :param alphabet: alphabet to use
129
    :type number: int, string, Alphanumber, float
130
    :type format: string
131
    :type alphabet: string
132
    """
133
    match = re.match(r"^(\d+)a(\d+)d", format)
134
    if not match or not match.groups() or len(match.groups()) != 2:
135
        if default is not _marker:
136
            return default
137
        raise ValueError("Format not supported: {}".format(format))
138
    matches = match.groups()
139
    num_chars = int(matches[0])
140
    num_digits = int(matches[1])
141
    try:
142
        return Alphanumber(number=number, num_chars=num_chars,
143
                           num_digits=num_digits, alphabet=alphabet)
144
    except ValueError as e:
145
        if default is not _marker:
146
            return default
147
        raise e
148
149
150
def to_decimal(alpha_number, alphabet=ALPHABET, default=_marker):
151
    """Converts an alphanumeric code (e.g AB12) to an integer
152
    :param alpha_number: representation of an alphanumeric code
153
    :param alphabet: alphabet to use when alpha_number is a non-int string
154
    :type number: int, string, Alphanumber, float
155
    :type alphabet: string
156
    """
157
    num = api.to_int(alpha_number, default=None)
158
    if num is not None:
159
        return num
160
161
    alpha_number = str(alpha_number)
162
    regex = re.compile(r"([A-Z]+)(\d+)", re.IGNORECASE)
163
    matches = re.findall(regex, alpha_number)
164
    if not matches:
165
        if default is not _marker:
166
            return default
167
        raise ValueError("Not a valid alpha number: {}".format(alpha_number))
168
169
    alpha = matches[0][0]
170
    number = int(matches[0][1])
171
    max_num = 10 ** len(matches[0][1]) - 1
172
    len_alphabet = len(alphabet)
173
    for pos_char, alpha_char in enumerate(reversed(alpha)):
174
        index_char = alphabet.find(alpha_char)
175
        number += (index_char * max_num * len_alphabet ** pos_char)
176
177
    return number
178