diff --git a/contrib_bots/lib/ConverterBot/docs.md b/contrib_bots/lib/ConverterBot/docs.md new file mode 100644 index 0000000..252b836 --- /dev/null +++ b/contrib_bots/lib/ConverterBot/docs.md @@ -0,0 +1,70 @@ +# Converter bot + +This bot allows users to perform conversions for various measurement units. + +## Usage + +Run this bot as described in [here](http://zulip.readthedocs.io/en/latest/bots-guide.html#how-to-deploy-a-bot). + +Use this bot with the following command + +`@convert ` + +This will convert `number`, given in the unit `unit_from`, to the unit `unit_to` +and print the result. + +* `number` can be any floating-point number, e.g. 12, 13.05, 0.002. +* `unit_from` and `unit_to` are two units from [the following](#supported-units) table in the same category. +* `unit_from` and `unit_to` can be preceded by [these](#supported-prefixes) prefixes. + +### Supported units + +| Category | Units | +| ----------------- | ----- | +| Area | square-centimeter (cm^2, cm2), square-decimeter (dm^2, dm2), square-meter (m^2, m2), square-kilometer (km^2, km2), square-inch (in^2, in2), square-foot (ft^2, ft2), square-yard (y^2, y2), square-mile (mi^2, mi2), are (a), hectare (ha), acre (ac) | +| Information | bit, byte | +| Length | centimeter (cm), decimeter (dm), meter (m), kilometer (km), inch (in), foot (ft), yard (y), mile (mi), nautical-mile (nmi) | +| Temperature | Kelvin (K), Celsius (C), Fahrenheit (F) | +| Volume | cubic-centimeter (cm^3, cm3), cubic-decimeter (dm^3, dm3), liter (l), cubic-meter (m^3, m3), cubic-inch (in^3, in3), fluid-ounce (fl-oz), cubic-foot (ft^3, ft3), cubic-yard (y^3, y3) | +| Weight | gram (g), kilogram (kg), ton (t), ounce (oz), pound (lb) | +| Cooking (metric only, U.S. and imperial units differ slightly) | teaspoon (tsp), tablespoon (tbsp), cup | + +### Supported prefixes + +| Prefix | Power of 10 | +| ------ | ----------- | +| atto | 10-18 | +| pico | 10-15 | +| femto | 10-12 | +| nano | 10-9 | +| micro | 10-6 | +| milli | 10-3 | +| centi | 10-2 | +| deci | 10-1 | +| deca | 101 | +| hecto | 102 | +| kilo | 103 | +| mega | 106 | +| giga | 109 | +| tera | 1012 | +| peta | 1015 | +| exa | 1018 | + +### Usage examples + +| Message | Response | +| ------- | ------ | +| `@convert 12 celsius fahrenheit` | 12.0 celsius = 53.600054 fahrenheit | +| `@convert 0.002 kilomile millimeter` | 0.002 kilomile = 3218688.0 millimeter | +| `@convert 31.5 square-mile ha | 31.5 square-mile = 8158.4625 ha | +| `@convert 56 g lb` | 56.0 g = 0.12345887 lb | + +## Notes + +* You can use multiple `@convert` statements in a message, the response will look accordingly: +![multiple-converts](multiple-converts.png) + +* Enter `@convert help` to display a quick overview of the converter's functionality. + +* For bits and bytes, the prefixes change the figure differently: 1 kilobyte is 1024 bytes, +1 megabyte is 1048576 bytes, etc. diff --git a/contrib_bots/lib/ConverterBot/multiple-converts.png b/contrib_bots/lib/ConverterBot/multiple-converts.png new file mode 100644 index 0000000..f198396 Binary files /dev/null and b/contrib_bots/lib/ConverterBot/multiple-converts.png differ diff --git a/contrib_bots/lib/converter.py b/contrib_bots/lib/converter.py new file mode 100644 index 0000000..eec2291 --- /dev/null +++ b/contrib_bots/lib/converter.py @@ -0,0 +1,275 @@ +# See readme.md for instructions on running this code. + +from __future__ import division +from past.utils import old_div + +import copy +from math import log10, floor + +# A dictionary allowing the conversion of each unit to its base unit. +# An entry consists of the unit's name, a constant number and a constant +# factor that need to be added and multiplied to convert the unit into +# the base unit in the last parameter. +UNITS = {'bit': [0, 1, 'bit'], + 'byte': [0, 8, 'bit'], + 'cubic-centimeter': [0, 0.000001, 'cubic-meter'], + 'cubic-decimeter': [0, 0.001, 'cubic-meter'], + 'liter': [0, 0.001, 'cubic-meter'], + 'cubic-meter': [0, 1, 'cubic-meter'], + 'cubic-inch': [0, 0.000016387064, 'cubic-meter'], + 'fluid-ounce': [0, 0.000029574, 'cubic-meter'], + 'cubic-foot': [0, 0.028316846592, 'cubic-meter'], + 'cubic-yard': [0, 0.764554857984, 'cubic-meter'], + 'teaspoon': [0, 0.0000049289216, 'cubic-meter'], + 'tablespoon': [0, 0.000014787, 'cubic-meter'], + 'cup': [0, 0.00023658823648491, 'cubic-meter'], + 'gram': [0, 1, 'gram'], + 'kilogram': [0, 1000, 'gram'], + 'ton': [0, 1000000, 'gram'], + 'ounce': [0, 28.349523125, 'gram'], + 'pound': [0, 453.59237, 'gram'], + 'kelvin': [0, 1, 'kelvin'], + 'celsius': [273.15, 1, 'kelvin'], + 'fahrenheit': [255.372222, 0.555555, 'kelvin'], + 'centimeter': [0, 0.01, 'meter'], + 'decimeter': [0, 0.1, 'meter'], + 'meter': [0, 1, 'meter'], + 'kilometer': [0, 1000, 'meter'], + 'inch': [0, 0.0254, 'meter'], + 'foot': [0, 0.3048, 'meter'], + 'yard': [0, 0.9144, 'meter'], + 'mile': [0, 1609.344, 'meter'], + 'nautical-mile': [0, 1852, 'meter'], + 'square-centimeter': [0, 0.0001, 'square-meter'], + 'square-decimeter': [0, 0.01, 'square-meter'], + 'square-meter': [0, 1, 'square-meter'], + 'square-kilometer': [0, 1000000, 'square-meter'], + 'square-inch': [0, 0.00064516, 'square-meter'], + 'square-foot': [0, 0.09290304, 'square-meter'], + 'square-yard': [0, 0.83612736, 'square-meter'], + 'square-mile': [0, 2589988.110336, 'square-meter'], + 'are': [0, 100, 'square-meter'], + 'hectare': [0, 10000, 'square-meter'], + 'acre': [0, 4046.8564224, 'square-meter']} + +PREFIXES = {'atto': -18, + 'femto': -15, + 'pico': -12, + 'nano': -9, + 'micro': -6, + 'milli': -3, + 'centi': -2, + 'deci': -1, + 'deca': 1, + 'hecto': 2, + 'kilo': 3, + 'mega': 6, + 'giga': 9, + 'tera': 12, + 'peta': 15, + 'exa': 18} + +ALIASES = {'a': 'are', + 'ac': 'acre', + 'c': 'celsius', + 'cm': 'centimeter', + 'cm2': 'square-centimeter', + 'cm3': 'cubic-centimeter', + 'cm^2': 'square-centimeter', + 'cm^3': 'cubic-centimeter', + 'dm': 'decimeter', + 'dm2': 'square-decimeter', + 'dm3': 'cubic-decimeter', + 'dm^2': 'square-decimeter', + 'dm^3': 'cubic-decimeter', + 'f': 'fahrenheit', + 'fl-oz': 'fluid-ounce', + 'ft': 'foot', + 'ft2': 'square-foot', + 'ft3': 'cubic-foot', + 'ft^2': 'square-foot', + 'ft^3': 'cubic-foot', + 'g': 'gram', + 'ha': 'hectare', + 'in': 'inch', + 'in2': 'square-inch', + 'in3': 'cubic-inch', + 'in^2': 'square-inch', + 'in^3': 'cubic-inch', + 'k': 'kelvin', + 'kg': 'kilogram', + 'km': 'kilometer', + 'km2': 'square-kilometer', + 'km^2': 'square-kilometer', + 'l': 'liter', + 'lb': 'pound', + 'm': 'meter', + 'm2': 'square-meter', + 'm3': 'cubic-meter', + 'm^2': 'square-meter', + 'm^3': 'cubic-meter', + 'mi': 'mile', + 'mi2': 'square-mile', + 'mi^2': 'square-mile', + 'nmi': 'nautical-mile', + 'oz': 'ounce', + 't': 'ton', + 'tbsp': 'tablespoon', + 'tsp': 'teaspoon', + 'y': 'yard', + 'y2': 'square-yard', + 'y3': 'cubic-yard', + 'y^2': 'square-yard', + 'y^3': 'cubic-yard'} + +HELP_MESSAGE = ('Converter usage:\n' + '`@convert `\n' + 'Converts `number` in the unit to ' + 'the and prints the result\n' + '`number`: integer or floating point number, e.g. 12, 13.05, 0.002\n' + ' and are two of the following units:\n' + '* square-centimeter (cm^2, cm2), square-decimeter (dm^2, dm2), ' + 'square-meter (m^2, m2), square-kilometer (km^2, km2),' + ' square-inch (in^2, in2), square-foot (ft^2, ft2), square-yard (y^2, y2), ' + ' square-mile(mi^2, mi2), are (a), hectare (ha), acre (ac)\n' + '* bit, byte\n' + '* centimeter (cm), decimeter(dm), meter (m),' + ' kilometer (km), inch (in), foot (ft), yard (y),' + ' mile (mi), nautical-mile (nmi)\n' + '* Kelvin (K), Celsius(C), Fahrenheit (F)\n' + '* cubic-centimeter (cm^3, cm3), cubic-decimeter (dm^3, dm3), liter (l), ' + 'cubic-meter (m^3, m3), cubic-inch (in^3, in3), fluid-ounce (fl-oz), ' + 'cubic-foot (ft^3, ft3), cubic-yard (y^3, y3)\n' + '* gram (g), kilogram (kg), ton (t), ounce (oz), pound(lb)\n' + '* (metric only, U.S. and imperial units differ slightly:) teaspoon (tsp), tablespoon (tbsp), cup\n\n\n' + 'Allowed prefixes are:\n' + '* atto, pico, femto, nano, micro, milli, centi, deci\n' + '* deca, hecto, kilo, mega, giga, tera, peta, exa\n\n\n' + 'Usage examples:\n' + '* `@convert 12 celsius fahrenheit`\n' + '* `@convert 0.002 kilomile millimeter`\n' + '* `@convert 31.5 square-mile ha`\n' + '* `@convert 56 g lb`\n') + +QUICK_HELP = 'Enter `@convert help` for help on using the converter.' + +def is_float(value): + try: + float(value) + return True + except ValueError: + return False + +# Rounds the number 'x' to 'digits' significant digits. +# A normal 'round()' would round the number to an absolute amount of +# fractional decimals, e.g. 0.00045 would become 0.0. +# 'round_to()' rounds only the digits that are not 0. +# 0.00045 would then become 0.0005. +def round_to(x, digits): + return round(x, digits-int(floor(log10(abs(x))))) + +class ConverterHandler(object): + ''' + This plugin allows users to make conversions between various units, + e.g. Celsius to Fahrenheit, or kilobytes to gigabytes. + It looks for messages of the format + '@convert ' + The message '@convert help' posts a short description of how to use + the plugin, along with a list of all supported units. + ''' + + def usage(self): + return ''' + This plugin allows users to make conversions between + various units, e.g. Celsius to Fahrenheit, + or kilobytes to gigabytes. It looks for messages of + the format '@convert ' + The message '@convert help' posts a short description of + how to use the plugin, along with a list of + all supported units. + ''' + + def triage_message(self, message, client): + return '@convert' in message['content'] + + def handle_message(self, message, client, state_handler): + content = message['content'] + + words = content.lower().split() + convert_indexes = [i for i, word in enumerate(words) if word == "@convert"] + results = [] + + for convert_index in convert_indexes: + if (convert_index + 1) < len(words) and words[convert_index + 1] == 'help': + results.append(HELP_MESSAGE) + continue + if (convert_index + 3) < len(words): + number = words[convert_index + 1] + unit_from = ALIASES.get(words[convert_index + 2], words[convert_index + 2]) + unit_to = ALIASES.get(words[convert_index + 3], words[convert_index + 3]) + exponent = 0 + + if not is_float(number): + results.append(number + ' is not a valid number. ' + QUICK_HELP) + continue + + number = float(number) + number_res = copy.copy(number) + + for key, exp in PREFIXES.items(): + if unit_from.startswith(key): + exponent += exp + unit_from = unit_from[len(key):] + if unit_to.startswith(key): + exponent -= exp + unit_to = unit_to[len(key):] + + uf_to_std = UNITS.get(unit_from, False) + ut_to_std = UNITS.get(unit_to, False) + + if uf_to_std is False: + results.append(unit_from + ' is not a valid unit. ' + QUICK_HELP) + if ut_to_std is False: + results.append(unit_to + ' is not a valid unit.' + QUICK_HELP) + if uf_to_std is False or ut_to_std is False: + continue + + base_unit = uf_to_std[2] + if uf_to_std[2] != ut_to_std[2]: + unit_from = unit_from.capitalize() if uf_to_std[2] == 'kelvin' else unit_from + results.append(unit_to.capitalize() + ' and ' + unit_from + + ' are not from the same category. ' + QUICK_HELP) + continue + + # perform the conversion between the units + number_res *= uf_to_std[1] + number_res += uf_to_std[0] + number_res -= ut_to_std[0] + number_res /= ut_to_std[1] + + if base_unit == 'bit': + number_res *= 1024 ** (old_div(exponent, float(3))) + else: + number_res *= 10 ** exponent + number_res = round_to(number_res, 7) + + results.append('{} {} = {} {}'.format(number, + words[convert_index + 2], + number_res, + words[convert_index + 3])) + + else: + results.append('Too few arguments given. ' + QUICK_HELP) + + new_content = '' + for idx, result in enumerate(results, 1): + new_content += ((str(idx) + '. conversion: ') if len(results) > 1 else '') + result + '\n' + + client.send_message(dict( + type='stream', + to=message['display_recipient'], + subject=message['subject'], + content=new_content, + )) + +handler_class = ConverterHandler