Passed
Push — master ( 194dfd...685ef9 )
by Ramon
05:11
created

bika.lims.alphanumber.Alphanumber.alpha_format()   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 3
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
# Copyright 2018 by it's authors.
6
# Some rights reserved. See LICENSE.rst, CONTRIBUTORS.rst.
7
8
import re
9
10
from bika.lims import api
11
12
_marker = object()
13
ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
14
15
16
class Alphanumber(object):
17
    """Represents an alphanumeric number
18
    """
19
20
    def __init__(self, number=0, num_chars=3, num_digits=3, alphabet=ALPHABET):
21
        if num_chars < 1:
22
            raise ValueError("num_chars param is lower than 1")
23
        if num_digits < 1:
24
            raise ValueError("num_digits param is lower than 1")
25
26
        self.alphabet = alphabet
27
        self.num_chars = num_chars
28
        self.num_digits = num_digits
29
        self.number = to_decimal(number, alphabet=alphabet)
30
31
        if self.number < 0:
32
            # TODO Support for negative values?
33
            raise ValueError("number is lower than 0")
34
35
    @property
36
    def alpha_format(self):
37
        return '%sa%sd' % (self.num_chars, self.num_digits)
38
39
    def __int__(self):
40
        return self.number
41
42
    def __index__(self):
43
        return self.__int__()
44
45
    def __str__(self):
46
        return self.__format__(self.alpha_format)
47
48
    def __repr__(self):
49
        return self.__str__()
50
51
    def __add__(self, other):
52
        number = self.__int__() + int(other)
53
        return Alphanumber(number=number, num_chars=self.num_chars,
54
                           num_digits=self.num_digits, alphabet=self.alphabet)
55
56
    def __sub__(self, other):
57
        number = self.__int__() - int(other)
58
        return Alphanumber(number=number, num_chars=self.num_chars,
59
                           num_digits=self.num_digits, alphabet=self.alphabet)
60
61
    def __lt__(self, other):
62
        return self.__int__() < int(other)
63
64
    def __gt__(self, other):
65
        return self.__int__() > int(other)
66
67
    def __eq__(self, other):
68
        return self.__int__() == int(other)
69
70
    def __format__(self, format):
71
        if self.alpha_format != format:
72
            return to_alpha(self.number, format, self.alphabet).format(format)
73
74
        base_format = "{alpha:%s>%s}{number:0%sd}" % (self.alphabet[0],
75
                                                      self.num_chars,
76
                                                      self.num_digits)
77
        alpha, number = self.parts()
78
        values = dict(alpha=alpha, number=number)
79
        return base_format.format(**values)
80
81
    def format(self, format):
82
        return self.__format__(format)
83
84
    def parts(self):
85
        """Returns the alphanumeric parts (chars + digits) of this Alphanum
86
        """
87
        def get_alpha(alpha_index, alphabet):
88
            if alpha_index >= len(alphabet):
89
                lead = get_alpha(alpha_index / len(alphabet), alphabet)
90
                trail = alphabet[alpha_index % len(alphabet)]
91
                return "{}{}".format(lead, trail)
92
            return alphabet[alpha_index]
93
94
        max_digits = 10 ** self.num_digits - 1
95
        alpha_index = abs(self.number) / max_digits
96
        alpha_number = abs(self.number) % max_digits
97
        # Note the 1 digit leap e.g. AA99 + 1 == AB01 (not AB00)
98
        if not alpha_number and abs(self.number):
99
            alpha_number = max_digits
100
            alpha_index -= 1
101
102
        alpha = get_alpha(alpha_index, self.alphabet)
103
        if len(alpha) > self.num_chars:
104
            raise ValueError("Out of bounds. Requires {} chars, {} set"
105
                             .format(len(alpha), self.num_chars))
106
107
        return alpha, alpha_number
108
109
110
def to_alpha(number, format, alphabet=ALPHABET, default=_marker):
111
    """Returns an Alphanumber object that represents the number in accordance
112
    with the format specified.
113
    :param number: a number representation used to create the Alphanumber
114
    :param format: the format to use. eg. '2a3d' for 2 chars and 3 digits
115
    :param alphabet: alphabet to use
116
    :type number: int, string, Alphanumber, float
117
    :type format: string
118
    :type alphabet: string
119
    """
120
    match = re.match(r"^(\d+)a(\d+)d", format)
121
    if not match or not match.groups() or len(match.groups()) != 2:
122
        if default is not _marker:
123
            return default
124
        raise ValueError("Format not supported: {}".format(format))
125
    matches = match.groups()
126
    num_chars = int(matches[0])
127
    num_digits = int(matches[1])
128
    try:
129
        return Alphanumber(number=number, num_chars=num_chars,
130
                           num_digits=num_digits, alphabet=alphabet)
131
    except ValueError as e:
132
        if default is not _marker:
133
            return default
134
        raise e
135
136
137
def to_decimal(alpha_number, alphabet=ALPHABET, default=_marker):
138
    """Converts an alphanumeric code (e.g AB12) to an integer
139
    :param alpha_number: representation of an alphanumeric code
140
    :param alphabet: alphabet to use when alpha_number is a non-int string
141
    :type number: int, string, Alphanumber, float
142
    :type alphabet: string
143
    """
144
    num = api.to_int(alpha_number, default=None)
145
    if num is not None:
146
        return num
147
148
    alpha_number = str(alpha_number)
149
    regex = re.compile(r"([A-Z]+)(\d+)", re.IGNORECASE)
150
    matches = re.findall(regex, alpha_number)
151
    if not matches:
152
        if default is not _marker:
153
            return default
154
        raise ValueError("Not a valid alpha number: {}".format(alpha_number))
155
156
    alpha = matches[0][0]
157
    number = int(matches[0][1])
158
    max_num = 10 ** len(matches[0][1]) - 1
159
    len_alphabet = len(alphabet)
160
    for pos_char, alpha_char in enumerate(reversed(alpha)):
161
        index_char = alphabet.find(alpha_char)
162
        number += (index_char * max_num * len_alphabet ** pos_char)
163
164
    return number
165