Passed
Push — 2.x ( b7a2dd...83455a )
by Ramon
07:43
created

bika.lims.utils.analysis.format_interim()   A

Complexity

Conditions 5

Size

Total Lines 37
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 18
dl 0
loc 37
rs 9.0333
c 0
b 0
f 0
cc 5
nop 2
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 copy
22
import math
23
24
from bika.lims import api
25
from bika.lims.interfaces import IAnalysisService
26
from bika.lims.interfaces import IBaseAnalysis
27
from bika.lims.interfaces import IReferenceSample
28
from bika.lims.interfaces.analysis import IRequestAnalysis
29
from bika.lims.utils import formatDecimalMark
30
from bika.lims.utils import format_supsub
31
32
33
def create_analysis(context, source, **kwargs):
34
    """Create a new Analysis.  The source can be an Analysis Service or
35
    an existing Analysis, and all possible field values will be set to the
36
    values found in the source object.
37
    :param context: The analysis will be created inside this object.
38
    :param source: The schema of this object will be used to populate analysis.
39
    :param kwargs: The values of any keys which match schema fieldnames will
40
    be inserted into the corresponding fields in the new analysis.
41
    :returns: Analysis object that was created
42
    :rtype: Analysis
43
    """
44
    # Ensure we have an object as source
45
    source = api.get_object(source)
46
    if not IBaseAnalysis.providedBy(source):
47
        raise ValueError("Type not supported: {}".format(repr(type(source))))
48
49
    # compute the id of the new analysis if necessary
50
    analysis_id = kwargs.get("id")
51
    if not analysis_id:
52
        keyword = source.getKeyword()
53
        analysis_id = generate_analysis_id(context, keyword)
54
55
    # get the service to be assigned to the analysis
56
    service = source
57
    if not IAnalysisService.providedBy(source):
58
        service = source.getAnalysisService()
59
60
    # use "Analysis" as portal_type unless explicitly set
61
    portal_type = kwargs.pop("portal_type", "Analysis")
62
63
    # initialize interims with those from the service if not explicitly set
64
    interim_fields = kwargs.pop("InterimFields", service.getInterimFields())
65
66
    # do not copy these fields from source
67
    skip_fields = [
68
        "Hidden",
69
        "Attachment",
70
        "Result",
71
        "ResultCaptureDate",
72
        "Worksheet"
73
    ]
74
75
    kwargs.update({
76
        "container": context,
77
        "portal_type": portal_type,
78
        "skip": skip_fields,
79
        "id": analysis_id,
80
        "AnalysisService": service,
81
        "InterimFields": interim_fields,
82
    })
83
    return api.copy_object(source, **kwargs)
84
85
86
def get_significant_digits(numeric_value):
87
    """
88
    Returns the precision for a given floatable value.
89
    If value is None or not floatable, returns None.
90
    Will return positive values if the result is below 1 and will
91
    return 0 values if the result is above or equal to 1.
92
    :param numeric_value: the value to get the precision from
93
    :returns: the numeric_value's precision
94
            Examples:
95
            numeric_value     Returns
96
            0               0
97
            0.22            1
98
            1.34            0
99
            0.0021          3
100
            0.013           2
101
            2               0
102
            22              0
103
    """
104
    try:
105
        numeric_value = float(numeric_value)
106
    except (TypeError, ValueError):
107
        return None
108
    if numeric_value == 0:
109
        return 0
110
    significant_digit = int(math.floor(math.log10(abs(numeric_value))))
111
    return 0 if significant_digit > 0 else abs(significant_digit)
112
113
114
def _format_decimal_or_sci(result, precision, threshold, sciformat):
115
    # Current result's precision is above the threshold?
116
    sig_digits = get_significant_digits(result)
117
118
    # Note that if result < 1, sig_digits > 0. Otherwise, sig_digits = 0
119
    # Eg:
120
    #       result = 0.2   -> sig_digit = 1
121
    #                0.002 -> sig_digit = 3
122
    #                0     -> sig_digit = 0
123
    #                2     -> sig_digit = 0
124
    # See get_significant_digits signature for further details!
125
    #
126
    # Also note if threshold is negative, the result will always be expressed
127
    # in scientific notation:
128
    # Eg.
129
    #       result=12345, threshold=-3, sig_digit=0 -> 1.2345e4 = 1.2345·10⁴
130
    #
131
    # So, if sig_digits is > 0, the power must be expressed in negative
132
    # Eg.
133
    #      result=0.0012345, threshold=3, sig_digit=3 -> 1.2345e-3=1.2345·10-³
134
    sci = sig_digits >= threshold and abs(
135
        threshold) > 0 and sig_digits <= precision
136
    sign = '-' if sig_digits > 0 else ''
137
    if sig_digits == 0 and abs(threshold) > 0 and abs(int(float(result))) > 0:
138
        # Number >= 1, need to check if the number of non-decimal
139
        # positions is above the threshold
140
        sig_digits = int(math.log(abs(float(result)), 10)) if abs(
141
            float(result)) >= 10 else 0
142
        sci = sig_digits >= abs(threshold)
143
144
    formatted = ''
145
    if sci:
146
        # First, cut the extra decimals according to the precision
147
        prec = precision if precision and precision > 0 else 0
148
        nresult = str("%%.%sf" % prec) % api.to_float(result, 0)
149
150
        if sign:
151
            # 0.0012345 -> 1.2345
152
            res = float(nresult) * (10 ** sig_digits)
153
        else:
154
            # Non-decimal positions
155
            # 123.45 -> 1.2345
156
            res = float(nresult) / (10 ** sig_digits)
157
        res = int(res) if res.is_integer() else res
158
159
        # Scientific notation
160
        if sciformat == 2:
161
            # ax10^b or ax10^-b
162
            formatted = "%s%s%s%s" % (res, "x10^", sign, sig_digits)
163
        elif sciformat == 3:
164
            # ax10<super>b</super> or ax10<super>-b</super>
165
            formatted = "%s%s%s%s%s" % (
166
                res, "x10<sup>", sign, sig_digits, "</sup>")
167
        elif sciformat == 4:
168
            # ax10^b or ax10^-b
169
            formatted = "%s%s%s%s" % (res, "·10^", sign, sig_digits)
170
        elif sciformat == 5:
171
            # ax10<super>b</super> or ax10<super>-b</super>
172
            formatted = "%s%s%s%s%s" % (
173
                res, "·10<sup>", sign, sig_digits, "</sup>")
174
        else:
175
            # Default format: aE^+b
176
            sig_digits = "%02d" % sig_digits
177
            formatted = "%s%s%s%s" % (res, "e", sign, sig_digits)
178
    else:
179
        # Decimal notation
180
        prec = precision if precision and precision > 0 else 0
181
        formatted = str("%%.%sf" % prec) % api.to_float(result, 0)
182
        if float(formatted) == 0 and '-' in formatted:
183
            # We don't want things like '-0.00'
184
            formatted = formatted.replace('-', '')
185
    return formatted
186
187
188
def format_uncertainty(analysis, decimalmark=".", sciformat=1):
189
    """Return formatted uncertainty value
190
191
    If the "Calculate precision from uncertainties" is enabled in
192
    the Analysis service, and
193
194
    a) If the non-decimal number of digits of the result is above
195
       the service's ExponentialFormatPrecision, the uncertainty will
196
       be formatted in scientific notation. The uncertainty exponential
197
       value used will be the same as the one used for the result. The
198
       uncertainty will be rounded according to the same precision as
199
       the result.
200
201
       Example:
202
       Given an Analysis with an uncertainty of 37 for a range of
203
       results between 30000 and 40000, with an
204
       ExponentialFormatPrecision equal to 4 and a result of 32092,
205
       this method will return 0.004E+04
206
207
    b) If the number of digits of the integer part of the result is
208
       below the ExponentialFormatPrecision, the uncertainty will be
209
       formatted as decimal notation and the uncertainty will be
210
       rounded one position after reaching the last 0 (precision
211
       calculated according to the uncertainty value).
212
213
       Example:
214
       Given an Analysis with an uncertainty of 0.22 for a range of
215
       results between 1 and 10 with an ExponentialFormatPrecision
216
       equal to 4 and a result of 5.234, this method will return 0.2
217
218
    If the "Calculate precision from Uncertainties" is disabled in the
219
    analysis service, the same rules described above applies, but the
220
    precision used for rounding the uncertainty is not calculated from
221
    the uncertainty neither the result. The fixed length precision is
222
    used instead.
223
224
    If the result is not floatable or no uncertainty defined, returns
225
    an empty string.
226
227
    The default decimal mark '.' will be replaced by the decimalmark
228
    specified.
229
230
    :param analysis: the analysis from which the uncertainty, precision
231
                     and other additional info have to be retrieved
232
    :param decimalmark: decimal mark to use. By default '.'
233
    :param sciformat: 1. The sci notation has to be formatted as aE^+b
234
                  2. The sci notation has to be formatted as ax10^b
235
                  3. As 2, but with super html entity for exp
236
                  4. The sci notation has to be formatted as a·10^b
237
                  5. As 4, but with super html entity for exp
238
                  By default 1
239
    :returns: the formatted uncertainty
240
    """
241
    try:
242
        result = float(analysis.getResult())
243
    except (ValueError, TypeError):
244
        pass
245
246
    uncertainty = analysis.getUncertainty()
247
248
    if not uncertainty:
249
        return ""
250
251
    # always convert exponential notation to decimal
252
    if "e" in uncertainty.lower():
253
        uncertainty = api.float_to_string(float(uncertainty))
254
255
    precision = -1
256
    # always get full precision of the uncertainty if user entered manually
257
    # => avoids rounding and cut-off
258
    allow_manual = analysis.getAllowManualUncertainty()
259
    manual_value = analysis.getField("Uncertainty").get(analysis)
260
    if allow_manual and manual_value:
261
        precision = uncertainty[::-1].find(".")
262
263
    if precision == -1:
264
        precision = analysis.getPrecision(result)
265
266
    # Scientific notation?
267
    # Get the default precision for scientific notation
268
    threshold = analysis.getExponentialFormatPrecision()
269
    formatted = _format_decimal_or_sci(
270
        uncertainty, precision, threshold, sciformat)
271
272
    # strip off trailing zeros and the orphane dot,
273
    # e.g.: 1.000000 -> 1
274
    if "." in formatted:
275
        formatted = formatted.rstrip("0").rstrip(".")
276
277
    return formatDecimalMark(formatted, decimalmark)
278
279
280
def format_numeric_result(analysis, result, decimalmark='.', sciformat=1):
281
    """
282
    Returns the formatted number part of a results value.  This is
283
    responsible for deciding the precision, and notation of numeric
284
    values in accordance to the uncertainty. If a non-numeric
285
    result value is given, the value will be returned unchanged.
286
287
    The following rules apply:
288
289
    If the "Calculate precision from uncertainties" is enabled in
290
    the Analysis service, and
291
292
    a) If the non-decimal number of digits of the result is above
293
       the service's ExponentialFormatPrecision, the result will
294
       be formatted in scientific notation.
295
296
       Example:
297
       Given an Analysis with an uncertainty of 37 for a range of
298
       results between 30000 and 40000, with an
299
       ExponentialFormatPrecision equal to 4 and a result of 32092,
300
       this method will return 3.2092E+04
301
302
    b) If the number of digits of the integer part of the result is
303
       below the ExponentialFormatPrecision, the result will be
304
       formatted as decimal notation and the resulta will be rounded
305
       in accordance to the precision (calculated from the uncertainty)
306
307
       Example:
308
       Given an Analysis with an uncertainty of 0.22 for a range of
309
       results between 1 and 10 with an ExponentialFormatPrecision
310
       equal to 4 and a result of 5.234, this method will return 5.2
311
312
    If the "Calculate precision from Uncertainties" is disabled in the
313
    analysis service, the same rules described above applies, but the
314
    precision used for rounding the result is not calculated from
315
    the uncertainty. The fixed length precision is used instead.
316
317
    For further details, visit
318
    https://jira.bikalabs.com/browse/LIMS-1334
319
320
    The default decimal mark '.' will be replaced by the decimalmark
321
    specified.
322
323
    :param analysis: the analysis from which the uncertainty, precision
324
                     and other additional info have to be retrieved
325
    :param result: result to be formatted.
326
    :param decimalmark: decimal mark to use. By default '.'
327
    :param sciformat: 1. The sci notation has to be formatted as aE^+b
328
                      2. The sci notation has to be formatted as ax10^b
329
                      3. As 2, but with super html entity for exp
330
                      4. The sci notation has to be formatted as a·10^b
331
                      5. As 4, but with super html entity for exp
332
                      By default 1
333
    :result: should be a string to preserve the decimal precision.
334
    :returns: the formatted result as string
335
    """
336
    try:
337
        result = float(result)
338
    except ValueError:
339
        return result
340
341
    # continuing with 'nan' result will cause formatting to fail.
342
    if math.isnan(result):
343
        return result
344
345
    # Scientific notation?
346
    # Get the default precision for scientific notation
347
    threshold = analysis.getExponentialFormatPrecision()
348
    precision = analysis.getPrecision(result)
349
    formatted = _format_decimal_or_sci(result, precision, threshold, sciformat)
350
    return formatDecimalMark(formatted, decimalmark)
351
352
353
def create_retest(analysis, **kwargs):
354
    """Creates a retest of the given analysis
355
    """
356
    if not IRequestAnalysis.providedBy(analysis):
357
        raise ValueError("Type not supported: {}".format(repr(type(analysis))))
358
359
    # Create a copy of the original analysis
360
    parent = api.get_parent(analysis)
361
    kwargs.update({
362
        "portal_type": api.get_portal_type(analysis),
363
        "RetestOf": analysis,
364
    })
365
    retest = create_analysis(parent, analysis, **kwargs)
366
367
    # Add the retest to the same worksheet, if any
368
    worksheet = analysis.getWorksheet()
369
    if worksheet:
370
        worksheet.addAnalysis(retest)
371
372
    return retest
373
374
375
def create_duplicate(analysis, **kwargs):
376
    """Creates a duplicate of the given analysis
377
    """
378
    if not IRequestAnalysis.providedBy(analysis):
379
        raise ValueError("Type not supported: {}".format(repr(type(analysis))))
380
381
    worksheet = analysis.getWorksheet()
382
    if not worksheet:
383
        raise ValueError("Cannot create a duplicate without worksheet")
384
385
    sample_id = analysis.getRequestID()
386
    kwargs.update({
387
        "portal_type": "DuplicateAnalysis",
388
        "Analysis": analysis,
389
        "Worksheet": worksheet,
390
        "ReferenceAnalysesGroupID": "{}-D".format(sample_id),
391
    })
392
393
    return create_analysis(worksheet, analysis, **kwargs)
394
395
396
def create_reference_analysis(reference_sample, source, **kwargs):
397
    """Creates a reference analysis inside the referencesample
398
    """
399
    ref = api.get_object(reference_sample)
400
    if not IReferenceSample.providedBy(ref):
401
        raise ValueError("Type not supported: {}".format(repr(type(ref))))
402
403
    # Set the type of the reference analysis
404
    ref_type = "b" if ref.getBlank() else "c"
405
    kwargs.update({
406
        "portal_type": "ReferenceAnalysis",
407
        "ReferenceType": ref_type,
408
    })
409
    return create_analysis(ref, source, **kwargs)
410
411
412
def generate_analysis_id(instance, keyword):
413
    """Generates a new analysis ID
414
    """
415
    count = 1
416
    new_id = keyword
417
    while new_id in instance.objectIds():
418
        new_id = "{}-{}".format(keyword, count)
419
        count += 1
420
    return new_id
421
422
423
def format_interim(interim_field, html=True):
424
    """Returns a copy of the interim field plus additional attributes suitable
425
    for visualization, like formatted_result and formatted_unit
426
    """
427
    separator = "<br/>" if html else ", "
428
429
    # copy to prevent persistent changes
430
    item = copy.deepcopy(interim_field)
431
432
    # get the raw value
433
    value = item.get("value", "")
434
    values = filter(None, api.to_list(value))
435
436
    # if choices, display texts instead of values
437
    choices = item.get("choices")
438
    if choices:
439
        # generate a {value:text} dict
440
        choices = choices.split("|")
441
        choices = dict(map(lambda ch: ch.strip().split(":"), choices))
442
443
        # set the text as the formatted value
444
        texts = [choices.get(v, "") for v in values]
445
        values = filter(None, texts)
446
447
    else:
448
        # default formatting
449
        setup = api.get_setup()
450
        decimal_mark = setup.getResultsDecimalMark()
451
        values = [formatDecimalMark(val, decimal_mark) for val in values]
452
453
    item["formatted_value"] = separator.join(values)
454
455
    # unit formatting
456
    unit = item.get("unit", "")
457
    item["formatted_unit"] = format_supsub(unit) if html else unit
458
459
    return item
460