.oO SearXNG Developer Documentation Oo.
Loading...
Searching...
No Matches
unit_converter.py
Go to the documentation of this file.
1# SPDX-License-Identifier: AGPL-3.0-or-later
2"""A plugin for converting measured values from one unit to another unit (a
3unit converter).
4
5The plugin looks up the symbols (given in the query term) in a list of
6converters, each converter is one item in the list (compare
7:py:obj:`ADDITIONAL_UNITS`). If the symbols are ambiguous, the matching units
8of measurement are evaluated. The weighting in the evaluation results from the
9sorting of the :py:obj:`list of unit converters<symbol_to_si>`.
10"""
11import typing
12import re
13import babel.numbers
14
15from flask_babel import gettext, get_locale
16
17from searx.wikidata_units import symbol_to_si
18from searx.plugins import Plugin, PluginInfo
19from searx.result_types import EngineResults
20
21if typing.TYPE_CHECKING:
22 from searx.search import SearchWithPlugins
23 from searx.extended_types import SXNG_Request
24 from searx.plugins import PluginCfg
25
26
27name = ""
28description = gettext("")
29
30plugin_id = ""
31preference_section = ""
32
33CONVERT_KEYWORDS = ["in", "to", "as"]
34
35
37 """Convert between units. The result is displayed in area for the
38 "answers".
39 """
40
41 id = "unit_converter"
42
43 def __init__(self, plg_cfg: "PluginCfg") -> None:
44 super().__init__(plg_cfg)
45
47 id=self.id,
48 name=gettext("Unit converter plugin"),
49 description=gettext("Convert between units"),
50 preference_section="general",
51 )
52
53 def post_search(self, request: "SXNG_Request", search: "SearchWithPlugins") -> EngineResults:
54 results = EngineResults()
55
56 # only convert between units on the first page
57 if search.search_query.pageno > 1:
58 return results
59
60 query = search.search_query.query
61 query_parts = query.split(" ")
62
63 if len(query_parts) < 3:
64 return results
65
66 for query_part in query_parts:
67 for keyword in CONVERT_KEYWORDS:
68 if query_part == keyword:
69 from_query, to_query = query.split(keyword, 1)
70 target_val = _parse_text_and_convert(from_query.strip(), to_query.strip())
71 if target_val:
72 results.add(results.types.Answer(answer=target_val))
73
74 return results
75
76
77# inspired from https://stackoverflow.com/a/42475086
78RE_MEASURE = r'''
79(?P<sign>[-+]?) # +/- or nothing for positive
80(\s*) # separator: white space or nothing
81(?P<number>[\d\.,]*) # number: 1,000.00 (en) or 1.000,00 (de)
82(?P<E>[eE][-+]?\d+)? # scientific notation: e(+/-)2 (*10^2)
83(\s*) # separator: white space or nothing
84(?P<unit>\S+) # unit of measure
85'''
86
87
88def _parse_text_and_convert(from_query, to_query) -> str | None:
89
90 # pylint: disable=too-many-branches, too-many-locals
91
92 if not (from_query and to_query):
93 return None
94
95 measured = re.match(RE_MEASURE, from_query, re.VERBOSE)
96 if not (measured and measured.group('number'), measured.group('unit')):
97 return None
98
99 # Symbols are not unique, if there are several hits for the from-unit, then
100 # the correct one must be determined by comparing it with the to-unit
101 # https://github.com/searxng/searxng/pull/3378#issuecomment-2080974863
102
103 # first: collecting possible units
104
105 source_list, target_list = [], []
106
107 for symbol, si_name, from_si, to_si, orig_symbol in symbol_to_si():
108
109 if symbol == measured.group('unit'):
110 source_list.append((si_name, to_si))
111 if symbol == to_query:
112 target_list.append((si_name, from_si, orig_symbol))
113
114 if not (source_list and target_list):
115 return None
116
117 source_to_si = target_from_si = target_symbol = None
118
119 # second: find the right unit by comparing list of from-units with list of to-units
120
121 for source in source_list:
122 for target in target_list:
123 if source[0] == target[0]: # compare si_name
124 source_to_si = source[1]
125 target_from_si = target[1]
126 target_symbol = target[2]
127
128 if not (source_to_si and target_from_si):
129 return None
130
131 _locale = get_locale() or 'en_US'
132
133 value = measured.group('sign') + measured.group('number') + (measured.group('E') or '')
134 value = babel.numbers.parse_decimal(value, locale=_locale)
135
136 # convert value to SI unit
137
138 if isinstance(source_to_si, (float, int)):
139 value = float(value) * source_to_si
140 else:
141 value = source_to_si(float(value))
142
143 # convert value from SI unit to target unit
144
145 if isinstance(target_from_si, (float, int)):
146 value = float(value) * target_from_si
147 else:
148 value = target_from_si(float(value))
149
150 if measured.group('E'):
151 # when incoming notation is scientific, outgoing notation is scientific
152 result = babel.numbers.format_scientific(value, locale=_locale)
153 else:
154 result = babel.numbers.format_decimal(value, locale=_locale, format='#,##0.##########;-#')
155
156 return f'{result} {target_symbol}'
EngineResults post_search(self, "SXNG_Request" request, "SearchWithPlugins" search)
None __init__(self, "PluginCfg" plg_cfg)
str|None _parse_text_and_convert(from_query, to_query)