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