Merge lp:~parnold-x/slingshot/conversion-plugin into lp:~elementary-pantheon/slingshot/trunk

Proposed by Djax
Status: Work in progress
Proposed branch: lp:~parnold-x/slingshot/conversion-plugin
Merge into: lp:~elementary-pantheon/slingshot/trunk
Diff against target: 422 lines (+362/-3)
4 files modified
lib/synapse-plugins/CMakeLists.txt (+6/-2)
lib/synapse-plugins/conversion-plugin.vala (+349/-0)
lib/synapse-plugins/vapi/monetary.vapi (+6/-0)
src/Backend/SynapseSearch.vala (+1/-1)
To merge this branch: bzr merge lp:~parnold-x/slingshot/conversion-plugin
Reviewer Review Type Date Requested Status
Danielle Foré Needs Fixing
Review via email: mp+229368@code.launchpad.net

Commit message

Add a unit conversion-plugin

Description of the change

Add a unit conversion-plugin in synapse. It uses GNU units (http://www.gnu.org/software/units/) so basically it can convert everything in everything.
The "conversion word" is set to "to" and should be localised. To test type for example "100$ to €" or "123l to gallons".
To support temperature conversion some additional code is added.
Most code in the plugin is needed to update the currency conversion rates. GNU units has a python script for this purpose but it needs root rights and additional python modules that are not included in elementary os so I implemented the update mechanism in vala.
The plugin checks for a update every 6 hours by checking if the currency rate file modification date is from today. If not it get the rates from timegenie.com, parses it and write it to the file.
At the first run it also copies the definition file from /usr/share/units/ to ~/.local/share/units and creates the directories if needed.

TODO: Add good icon. Till now it takes the calculator icon.

To post a comment you must log in.
Revision history for this message
Danielle Foré (danrabbit) wrote :

hmm this doesn't seem to do anything for me. I don't have a /usr/share/units or ~/.local/share/units. Is there a dependency that wasn't added to cmake?

review: Needs Fixing
Revision history for this message
Djax (parnold-x) wrote :

It depends on GNU units. So you need to install the "units" package. Same as the calculator depends on the "bc" package that i installed by default.

Revision history for this message
Danielle Foré (danrabbit) wrote :

It would probably be good to show the final units in the result like 100 meters to miles = "0.06 miles"

Also, rounding off after the second decimal might be good :)

Can we also add "as" and "in"? so we could have "100 liters in gallons" etc

433. By Djax

conversion word = to,as,in and formating the output

434. By Djax

reformating the output

Revision history for this message
Djax (parnold-x) wrote :

Added "as" and "in". Should the "conversion words" stay the same in all languages or should they be localised?
Also rounding the output and add the unit to the result. When having very big or small numbers it prints e+14 or e-9. I think this is better than 100000000000000. OK?

Is there a way to ammend to a commit like in git in bazaar?

Revision history for this message
Danielle Foré (danrabbit) wrote :

I imagine it should probably be localized. I'm not sure how many words make sense in all languages so I wonder if there's a way to define an array that localizations can fill with however many words make sense for them?

If possible, I would prefer something more human readable like "100 Million Miles" until we get over billion (I think anything more than that is probably not common language) and then e+14 probably makes sense.

You can do 'uncommit' and 'commit -m "whatever"' and then 'bzr push --overwrite'

435. By Djax

localisation and formating output

Revision history for this message
Djax (parnold-x) wrote :

Good to know. Thanks for the bzr commands.
Yep, no problem. I added a comment for the localisation guys. But if they localise it wrong the regex match will fail.

/* @ localization if you want use mutiple words, split them with a "|" between the words.
   If it's just one word use "to". Don't use whitspaces before or after the words!
*/
private const string CONVERSION_WORD = _("to|as|in");
private const string MILLION = _("million");
private const string BILLION = _("billion");

I also added formating the result to million and billion. And when over 999 billion it uses the e+xx format. Don't know if you also want thousand? Till now it prints 120000 as number or do you want 1 tousand till 999 thousand also reformated?

Also happy to do anything else :) I am just couple days offline starting tomorrow.

Revision history for this message
Danielle Foré (danrabbit) wrote :

Hey I hate to mention this after the fact xD But I was just thinking "There must be a library for this" and it turns out there is a library that will format strings based on locale for decimal places and separators and all that. So I guess we should probably be using that :p

http://www.gnu.org/software/libc/manual/html_node/Formatting-Numbers.html

436. By Djax

using libc strfmon function

Revision history for this message
Djax (parnold-x) wrote :

Hey, I am back online.
I tried to write a vapi file that use the libc function and integrated it into cmake. I hope this is the right way since this is my first .vapi file. It seems to work well.

437. By Djax

replacement after split

438. By Djax

improved regex and codestyle thanks to jacobparker1992; print small and very big numbers scientifically

Revision history for this message
Djax (parnold-x) wrote :

Added improved regex match for the temperatures. Thanks to Jakob Parker.

For the record:

I wondered if we now exclude other units and the symbols C and F are assign to different units.
http://en.wikipedia.org/wiki/Coulomb
http://en.wikipedia.org/wiki/Farad

celsius is assigned to °C
fahrenheit to °F
and rankin to °R
only kelvin is assigned to K without the degree symbol.

But google does it also without a degree symbol so I think temperature conversion is used way more often and converting in Farad/Coulomb is still possible with the full names.
https://www.google.de/search?q=100C+to+F&oq=100C+to+F

Revision history for this message
Jacob Parker (jacobparker1992) wrote :

Don't kill me, but I changed the code style once again to match the other files in the plugins directory (which is apparently different to the elementary code style :P).

How are digits handled? We pick up '1.3 meters to yards', but I don't think we handle the funny countries that would write '1,3' instead. I'm not sure why we pick up '1.3' though---is that somehow part of the digit regex (JS considers \d to only be 0-9)?

- Jacob

439. By Djax

allow , as digit seperator

Revision history for this message
Djax (parnold-x) wrote :

The other plugins are just imported from synapse, so no need to adapt to their code style.
Ah yeah, my "funny" country uses 1,3 but it is easy to fix.
"1.3 meters to yards". The match_regex splits just at " to " with no digit before the whitespace. So no problem with what digit is used.
In the temperature case there was a problem to match the digit in that case but I fixed that also.

Revision history for this message
Jacob Parker (jacobparker1992) wrote :

Any chance this could get another review?

Revision history for this message
Djax (parnold-x) wrote :

Note: Some superscripts look slightly missplaced. This is caused by the font size of the search result. With 12pt or greater everything looks ok.

Revision history for this message
Danielle Foré (danrabbit) wrote :

Bumping. Needs to be merged into trunk

review: Needs Fixing

Unmerged revisions

439. By Djax

allow , as digit seperator

438. By Djax

improved regex and codestyle thanks to jacobparker1992; print small and very big numbers scientifically

437. By Djax

replacement after split

436. By Djax

using libc strfmon function

435. By Djax

localisation and formating output

434. By Djax

reformating the output

433. By Djax

conversion word = to,as,in and formating the output

432. By Djax

add unit conversion-plugin

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/synapse-plugins/CMakeLists.txt'
--- lib/synapse-plugins/CMakeLists.txt 2014-06-29 14:50:36 +0000
+++ lib/synapse-plugins/CMakeLists.txt 2014-08-16 22:26:39 +0000
@@ -6,6 +6,8 @@
6 gio-unix-2.06 gio-unix-2.0
7 gee-0.87 gee-0.8
8 gtk+-3.08 gtk+-3.0
9 libsoup-2.4
10 libxml-2.0
9)11)
1012
11pkg_check_modules(PLUGINS_DEPS REQUIRED ${PLUGINS_PKG})13pkg_check_modules(PLUGINS_DEPS REQUIRED ${PLUGINS_PKG})
@@ -13,6 +15,7 @@
13set(PLUGINS_SOURCE15set(PLUGINS_SOURCE
14 calculator-plugin.vala16 calculator-plugin.vala
15 command-plugin.vala17 command-plugin.vala
18 conversion-plugin.vala
16 desktop-file-plugin.vala19 desktop-file-plugin.vala
17)20)
1821
@@ -23,6 +26,8 @@
23PACKAGES26PACKAGES
24 ${PLUGINS_PKG}27 ${PLUGINS_PKG}
25 synapse-core28 synapse-core
29CUSTOM_VAPIS
30 vapi/monetary.vapi
26OPTIONS31OPTIONS
27 --vapidir=${CMAKE_BINARY_DIR}/lib/synapse-core32 --vapidir=${CMAKE_BINARY_DIR}/lib/synapse-core
28GENERATE_VAPI33GENERATE_VAPI
@@ -43,5 +48,4 @@
43 SOVERSION ${PLUGINS_LIB_SOVERSION}48 SOVERSION ${PLUGINS_LIB_SOVERSION}
44)49)
4550
46target_link_libraries (${PLUGINS_LIBRARY_NAME} ${PLUGINS_DEPS_LIBRARIES} synapse-core)51target_link_libraries (${PLUGINS_LIBRARY_NAME} ${PLUGINS_DEPS_LIBRARIES} synapse-core)
47
48\ No newline at end of file52\ No newline at end of file
4953
=== added file 'lib/synapse-plugins/conversion-plugin.vala'
--- lib/synapse-plugins/conversion-plugin.vala 1970-01-01 00:00:00 +0000
+++ lib/synapse-plugins/conversion-plugin.vala 2014-08-16 22:26:39 +0000
@@ -0,0 +1,349 @@
1/*
2 * Copyright (C) 2014 Peter Arnold <parnold1@gmail.com>
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
17 *
18 * Authored by Peter Arnold <parnold1@gmail.com>
19 *
20 */
21
22namespace Synapse
23{
24 errordomain IOError {
25 FILE_NOT_FOUND
26 }
27
28 public class ConversionPlugin: Object, Activatable, ItemProvider {
29 public bool enabled { get; set; default = true; }
30
31 public void activate () {}
32
33 public void deactivate () {}
34
35 private class Result: Object, Match {
36 // from Match interface
37 public string title { get; construct set; }
38 public string description { get; set; }
39 public string icon_name { get; construct set; }
40 public bool has_thumbnail { get; construct set; }
41 public string thumbnail_path { get; construct set; }
42 public MatchType match_type { get; construct set; }
43
44 public int default_relevancy { get; set; default = 0; }
45
46 public Result (string result) {
47 Object (match_type: MatchType.TEXT,
48 title: result,
49 description: result,
50 has_thumbnail: false,
51 icon_name: "accessories-calculator");
52 }
53 }
54
55 static void register_plugin () {
56 DataSink.PluginRegistry.get_default ().register_plugin (
57 typeof (ConversionPlugin),
58 _ ("Conversion"),
59 _ ("Conversion of units."),
60 "accessories-calculator",
61 register_plugin,
62 Environment.find_program_in_path ("units") != null,
63 _ ("units is not installed")
64 );
65 }
66
67 static construct {
68 register_plugin ();
69 }
70
71 /* @ localization if you want use mutiple words, split them with a "|" between the words.
72 If it's just one word use "to". Don't use whitspaces before or after the words!
73 */
74 private const string CONVERSION_WORD = _("to|as|in");
75 private Regex match_regex;
76 private Regex temp_regex;
77 private Regex digit_regex;
78
79 construct {
80 try {
81 /* regex to match the conversion words (to, as, in) with no digit before the whitespace in cases the conversion word
82 has the same name as a unit for example in and in (inch)
83 */
84 match_regex = new Regex (@"(?<=\\D)\\s*\\b(?:$CONVERSION_WORD)\\b\\s*",
85 RegexCompileFlags.OPTIMIZE);
86 // regex to match the temperature units
87 temp_regex = new Regex ("(?i)(?<=[\\d\\s])[FCKR]\\b|fahrenheit|celsius|kelvin|rankine",
88 RegexCompileFlags.OPTIMIZE);
89 digit_regex = new Regex ("(\\d*\\.?\\d*)",
90 RegexCompileFlags.OPTIMIZE);
91 } catch (Error e) {
92 Utils.Logger.error (this, "Error creating regexp.");
93 }
94
95 try {
96 // set a timer that wake ups every 6hours = 21600s to check if the currency rates should be updated
97 check_for_update.begin ();
98 Timeout.add_seconds_full (Priority.DEFAULT, 21600, () => {
99 check_for_update.begin ();
100 return true;
101 });
102 } catch (Error e) {
103 Utils.Logger.error (this, "Error updating the currency rates");
104 }
105
106 }
107
108 public bool handles_query (Query query) {
109 return (QueryFlags.ACTIONS in query.query_type);
110 }
111
112 static bool regex_function (MatchInfo info, StringBuilder resource) {
113 string match = info.fetch (0).slice (0, 1).up ();
114 resource.append (@"temp$match ");
115 return false;
116 }
117
118 public async ResultSet? search (Query query) throws SearchError {
119 string input = query.query_string;
120 bool matched = match_regex.match (input);
121
122 if (!matched) {
123 query.check_cancellable ();
124 return null;
125 }
126
127 try {
128 input = input.replace ("²", "^2").replace ("³", "^3").replace ("°", "").replace(",",".");
129
130 // to support the conversion of temperatures see http://www.gnu.org/software/units/manual/units.html#Temperature-Conversions
131 bool temparaturematch = temp_regex.match (input);
132
133 if (temparaturematch)
134 input = temp_regex.replace_eval (input, input.length, 0, RegexMatchFlags.NOTBOL, (RegexEvalCallback) regex_function);
135
136 string[] inputsplit = match_regex.split (input);
137
138 if (temparaturematch) {
139 MatchInfo info;
140 digit_regex.match (inputsplit[0], 0, out info);
141 string digit = info.fetch (0);
142 inputsplit[0] = inputsplit[0].replace (digit, "") + @"($digit)";
143 }
144
145 Pid pid;
146 int read_fd, write_fd;
147 string[] argv = {"units", "-1", "-f", Environment.get_home_dir () + "/.local/share/units/definitions.units", inputsplit[0], inputsplit[1]};
148 string? solution = null;
149
150 Process.spawn_async_with_pipes (null, argv, null,
151 SpawnFlags.SEARCH_PATH,
152 null, out pid, out write_fd, out read_fd);
153
154 UnixInputStream read_stream = new UnixInputStream (read_fd, true);
155 DataInputStream bc_output = new DataInputStream (read_stream);
156 solution = yield bc_output.read_line_async (Priority.DEFAULT_IDLE, query.cancellable);
157
158 if (solution != null && (solution.contains ("*") || temparaturematch)) {
159 query.check_cancellable ();
160 solution = solution.replace ("*", "");
161 solution = solution.chug ();
162 bool res = double.try_parse (solution);
163 if (!res)
164 return null;
165
166 double d = double.parse (solution);
167 string digit;
168 // the smallest output of the strfmon function is 0.01 and > 10^11 is not really readable in float format
169 // so print it scientifically in those cases
170 if (d >= 0.01 && d < 999999999999.9) {
171 char buffer[100];
172 Monetary.strfmon(buffer, "%!n", d);
173 digit = (string) buffer;
174 } else
175 digit = print_scientifically (d);
176
177 string unit = " " + inputsplit[1];
178 unit = unit.replace (" tempK", " K").replace (" temp", " °");
179
180 Result result = new Result (digit + unit);
181 ResultSet results = new ResultSet ();
182 results.add (result, Match.Score.AVERAGE);
183
184 return results;
185 }
186 } catch (Error err) {
187 if (!query.is_cancelled ())
188 warning ("%s", err.message);
189 }
190
191 return null;
192 }
193
194 // prints the %g output of printf in "x 10⁷⁷" format
195 private string print_scientifically (double d) {
196 string digit = "%g".printf(d);
197 if (!digit.contains ("e"))
198 return digit;
199 string exponent = digit.substring (digit.last_index_of ("e")+2);
200 if (exponent.has_prefix ("0"))
201 exponent = exponent.replace ("0","");
202 exponent = exponent.replace ("0","⁰").replace ("1","¹").replace ("2","²").replace ("3","³").replace ("4","⁴").replace ("5","⁵")
203 .replace ("6","⁶").replace ("7","⁷").replace ("8","⁸").replace ("9","⁹");
204 if (digit.substring (digit.last_index_of ("e")+1,1) == "-")
205 exponent = "⁻" + exponent;
206 return digit.substring (0,digit.last_index_of ("e")) + " x 10" + exponent;
207 }
208
209 private async void check_for_update () {
210 var file = File.new_for_path (Environment.get_home_dir () + "/.local/share/units/currency.units");
211
212 if (!file.query_exists ()) {
213 try {
214 //copy data to user directory to be able to modifiy it without root rights
215 var dir = File.new_for_path (Environment.get_home_dir () + "/.local/share/units/");
216 if (!dir.query_exists ())
217 dir.make_directory ();
218 var definitions_file = File.new_for_path ("/usr/share/units/definitions.units");
219 var target_definitions = File.new_for_path (Environment.get_home_dir()+"/.local/share/units/definitions.units");
220 if (!target_definitions.query_exists ())
221 definitions_file.copy (target_definitions, FileCopyFlags.NONE);
222 var currency_file = File.new_for_path ("/usr/share/units/currency.units");
223 currency_file.copy (file, FileCopyFlags.NONE);
224 } catch (Error e) {
225 Utils.Logger.error (this, "Failed to copy /usr/share/units/definitions.units");
226 }
227 }
228 if (file.query_exists ()) {
229 try {
230 var file_info = file.query_info ("*", FileQueryInfoFlags.NONE);
231 var modification_date = new DateTime.from_timeval_local (file_info.get_modification_time ());
232 var now = new DateTime.now_local ();
233 // timegenie update their rates once a day
234 if (now.get_day_of_year () != modification_date.get_day_of_year ())
235 update (file, now);
236 } catch (Error e) {
237 Utils.Logger.error (this, "Failed to update '%s'".printf (file.get_path ()));
238 }
239 }
240 }
241
242 private void update (File file, DateTime now) throws Error {
243 file.delete ();
244 var dos = new DataOutputStream (file.create (FileCreateFlags.REPLACE_DESTINATION));
245 Array<string> codes;
246 Array<string> names;
247 Array<string> rates;
248 // load and write data from timegenie
249 load_currency_rates_data (out codes, out names, out rates);
250 write_currency_data (dos, codes, names, rates, now);
251 }
252
253 private void write_currency_data (DataOutputStream dos, Array<string> codes, Array<string> names, Array<string> values, DateTime now) throws Error {
254 var builder = new StringBuilder ("# ISO Currency Codes\n\n");
255
256 for (int i = 0; i < codes.length; i++)
257 builder.append ("%s%s%s\n".printf (codes.index (i), string.nfill (15, ' '), names.index (i)));
258
259 builder.append ("\n# Currency exchange rates from Time Genie (www.timegenie.com) from %s\n\n".printf (now.format ("%F")));
260
261 for (int i = 0; i < codes.length; i++)
262 builder.append ("%s%s%s\n".printf (names.index (i), string.nfill (30-names.index (i).length, ' '), values.index (i)));
263
264 uint8[] data = builder.str.data;
265 long written = 0;
266
267 while (written < data.length)
268 written += dos.write (data[written:data.length]);
269 }
270
271 private void load_currency_rates_data (out Array<string> codes, out Array<string> names, out Array<string> values) throws IOError {
272 codes = new Array<string> ();
273 names = new Array<string> ();
274 values = new Array<string> ();
275 var tmp_values = new Array<string> ();
276 string url = "http://rss.timegenie.com/forex.xml";
277
278 var session = new Soup.Session ();
279 var msg = new Soup.Message ("GET", url);
280 session.send_message (msg);
281 Xml.Doc* doc = Xml.Parser.parse_memory (
282 (string) (msg.response_body.data),
283 (int) (msg.response_body.length));
284 if (doc == null)
285 throw new IOError.FILE_NOT_FOUND("Failed to parse http://rss.timegenie.com/forex.xml");
286 Xml.Node* root = doc->get_root_element ();
287 if (root == null) {
288 delete doc;
289 throw new IOError.FILE_NOT_FOUND ("The xml file is empty");
290 }
291 int index_usd = -1;
292 int index_euro = -1;
293 int index = 0;
294 for (Xml.Node* iter = root->children; iter != null; iter = iter->next) {
295 if (iter->type == Xml.ElementType.ELEMENT_NODE) {
296 if (iter->name == "data") {
297 for (Xml.Node* iter2 = iter->children; iter2 != null; iter2 = iter2->next) {
298 if (iter2->type == Xml.ElementType.ELEMENT_NODE) {
299 switch (iter2->name) {
300 case "code":
301 var content = get_node_content (iter2);
302 if (content != "") {
303 if (content == "USD")
304 index_usd = index;
305 else if (content == "EUR")
306 index_euro = index;
307 codes.append_val (content);
308 }
309 break;
310 case "description":
311 var content = get_node_content (iter2);
312 if (content != "") {
313 if (content == "Anguilla (ECD)") content = "eastcaribbeandollar";
314 content = content.replace (" ", "").down ();
315 names.append_val (content);
316 }
317 break;
318 case "rate":
319 var content = get_node_content (iter2);
320 if (content != "")
321 tmp_values.append_val (content);
322 break;
323 default:
324 // do nothing
325 break;
326 }
327 }
328 }
329 index++;
330 }
331 }
332 }
333 delete doc;
334 for (int i = 0; i < tmp_values.length; i++) {
335 if (i == index_euro) values.append_val (tmp_values.index (index_usd) + " US$");
336 else if (i == index_usd) values.append_val ("1 US$");
337 else values.append_val ("1|" + tmp_values.index (i) + " euro");
338 }
339 }
340
341 private string get_node_content (Xml.Node* node) {
342 for (Xml.Node* iter = node->children; iter != null; iter = iter->next) {
343 if (iter->type == Xml.ElementType.TEXT_NODE)
344 return iter->get_content ();
345 }
346 return "";
347 }
348 }
349}
0\ No newline at end of file350\ No newline at end of file
1351
=== added directory 'lib/synapse-plugins/vapi'
=== added file 'lib/synapse-plugins/vapi/monetary.vapi'
--- lib/synapse-plugins/vapi/monetary.vapi 1970-01-01 00:00:00 +0000
+++ lib/synapse-plugins/vapi/monetary.vapi 2014-08-16 22:26:39 +0000
@@ -0,0 +1,6 @@
1[CCode (cheader_filename = "monetary.h")]
2public class Monetary {
3 [CCode(cname = "strfmon")]
4 public static ssize_t strfmon(char[] s, string format, double data);
5
6}
0\ No newline at end of file7\ No newline at end of file
18
=== modified file 'src/Backend/SynapseSearch.vala'
--- src/Backend/SynapseSearch.vala 2014-06-29 14:50:36 +0000
+++ src/Backend/SynapseSearch.vala 2014-08-16 22:26:39 +0000
@@ -23,6 +23,7 @@
23 private static Type[] plugins = {23 private static Type[] plugins = {
24 typeof (Synapse.CalculatorPlugin),24 typeof (Synapse.CalculatorPlugin),
25 typeof (Synapse.CommandPlugin),25 typeof (Synapse.CommandPlugin),
26 typeof (Synapse.ConversionPlugin),
26 typeof (Synapse.DesktopFilePlugin)27 typeof (Synapse.DesktopFilePlugin)
27 };28 };
2829
@@ -161,4 +162,3 @@
161 }162 }
162 }163 }
163}164}
164

Subscribers

People subscribed via source and target branches