Repository: MycroftAI/lingua-franca (language: python)

Issue:
Russian language support
I added support for the Russian (ru-ru) language. It is based on the source code for Czech and Polish, which are also Slavic languages (many thanks to the authors @Tony763 and @rafaljanicki!)

The support is not perfect at the moment (you can see it from the commented tests), but it covers most cases (thanks a lot for the tests too!) I would like to suggest adding it as it is. This will allow everyone to fully test it.

Tests:
- `pytest test/test_format.py`
- `pytest test/test_format_ru.py`
- `pytest test/test_parse_ru.py`

Related PR to mycroft-core: https://github.com/MycroftAI/mycroft-core/pull/3014
Russian language support
I added support for the Russian (ru-ru) language. It is based on the source code for Czech and Polish, which are also Slavic languages (many thanks to the authors @Tony763 and @rafaljanicki!)

The support is not perfect at the moment (you can see it from the commented tests), but it covers most cases (thanks a lot for the tests too!) I would like to suggest adding it as it is. This will allow everyone to fully test it.

Tests:
- `pytest test/test_format.py`
- `pytest test/test_format_ru.py`
- `pytest test/test_parse_ru.py`

Related PR to mycroft-core: https://github.com/MycroftAI/mycroft-core/pull/3014


Discussion / hints:

Fix patch:
diff --git a/lingua_franca/internal.py b/lingua_franca/internal.py
index 38208ab0..bb2e04a2 100644
--- a/lingua_franca/internal.py
+++ b/lingua_franca/internal.py
@@ -10,7 +10,7 @@
 
 
 _SUPPORTED_LANGUAGES = ("ca", "cs", "da", "de", "en", "es", "fr", "hu",
-                        "it", "nl", "pl", "pt", "sl", "sv", "fa")
+                        "it", "nl", "pl", "pt", "ru", "sl", "sv", "fa")
 
 _SUPPORTED_FULL_LOCALIZATIONS = ("ca-es", "cs-cz", "da-dk", "de-de",
                                  "en-au", "en-us", "es-es", "fr-fr",
diff --git a/lingua_franca/lang/common_data_ru.py b/lingua_franca/lang/common_data_ru.py
new file mode 100644
index 00000000..dfa795fe
--- /dev/null
+++ b/lingua_franca/lang/common_data_ru.py
@@ -0,0 +1,304 @@
+#
+# Copyright 2017 Mycroft AI Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+from collections import OrderedDict
+
+
+_NUM_STRING_RU = {
+    0: 'ноль',
+    1: 'один',
+    2: 'два',
+    3: 'три',
+    4: 'четыре',
+    5: 'пять',
+    6: 'шесть',
+    7: 'семь',
+    8: 'восемь',
+    9: 'девять',
+    10: 'десять',
+    11: 'одиннадцать',
+    12: 'двенадцать',
+    13: 'тринадцать',
+    14: 'четырнадцать',
+    15: 'пятнадцать',
+    16: 'шестнадцать',
+    17: 'семнадцать',
+    18: 'восемнадцать',
+    19: 'девятнадцать',
+    20: 'двадцать',
+    30: 'тридцать',
+    40: 'сорок',
+    50: 'пятьдесят',
+    60: 'шестьдесят',
+    70: 'семьдесят',
+    80: 'восемьдесят',
+    90: 'девяносто',
+    100: 'сто',
+    200: 'двести',
+    300: 'триста',
+    400: 'четыреста',
+    500: 'пятьсот',
+    600: 'шестьсот',
+    700: 'семьсот',
+    800: 'восемьсот',
+    900: 'девятьсот'
+}
+
+
+_FRACTION_STRING_RU = {
+    2: 'половина',
+    3: 'треть',
+    4: 'четверть',
+    5: 'пятая',
+    6: 'шестая',
+    7: 'седьмая',
+    8: 'восьмая',
+    9: 'девятая',
+    10: 'десятая',
+    11: 'одиннадцатая',
+    12: 'двенадцатая',
+    13: 'тринадцатая',
+    14: 'четырнадцатая',
+    15: 'пятнадцатая',
+    16: 'шестнадцатая',
+    17: 'семнадцатая',
+    18: 'восемнадцатая',
+    19: 'девятнадцатая',
+    20: 'двадцатая',
+    30: 'тридцатая',
+    40: 'сороковая',
+    50: 'пятидесятая',
+    60: 'шестидесятая',
+    70: 'семидесятая',
+    80: 'восьмидесятая',
+    90: 'девяностая',
+    1e2: 'сотая',
+    1e3: 'тысячная',
+    1e6: 'миллионная',
+    1e9: 'миллиардная'
+}
+
+
+_SHORT_SCALE_RU = OrderedDict([
+    (1e3, 'тысяча'),
+    (1e6, "миллион"),
+    (1e9, "миллиард"),
+    (1e12, "триллион"),
+    (1e15, "квадриллион"),
+    (1e18, "квинтиллион"),
+    (1e21, "секстиллион"),
+    (1e24, "септиллион"),
+    (1e27, "октиллион"),
+    (1e30, "нониллион"),
+    (1e33, "дециллион"),
+    (1e36, "ундециллион"),
+    (1e39, "дуодециллион"),
+    (1e42, "тредециллион"),
+    (1e45, "кваттордециллион"),
+    (1e48, "квиндециллион"),
+    (1e51, "сексдециллион"),
+    (1e54, "септендециллион"),
+    (1e57, "октодециллион"),
+    (1e60, "новемдециллион"),
+    (1e63, "вигинтиллион"),
+    (1e66, "унвигинтиллион"),
+    (1e69, "дуовигинтиллион"),
+    (1e72, "тревигинтиллион"),
+    (1e75, "кватторвигинтиллион"),
+    (1e78, "квинвигинтиллион"),
+    (1e81, "секснвигинтиллион"),
+    (1e84, "септенвигинтиллион"),
+    (1e87, "октовигинтиллион"),
+    (1e90, "новемвигинтиллион"),
+    (1e93, "тригинтиллион"),
+])
+
+
+_LONG_SCALE_RU = OrderedDict([
+    (1e3, 'тысяча'),
+    (1e6, "миллион"),
+    (1e9, "миллиард"),
+    (1e12, "биллион"),
+    (1e15, "биллиард"),
+    (1e18, "триллион"),
+    (1e21, "триллиард"),
+    (1e24, "квадриллион"),
+    (1e27, "квадриллиард"),
+    (1e30, "квинтиллион"),
+    (1e33, "квинтиллиард"),
+    (1e36, "секстиллион"),
+    (1e39, "секстиллиард"),
+    (1e42, "септиллион"),
+    (1e45, "септиллиард"),
+    (1e48, "октиллион"),
+    (1e51, "октиллиард"),
+    (1e54, "нониллион"),
+    (1e57, "нониллиард"),
+    (1e60, "дециллион"),
+    (1e63, "дециллиард"),
+    (1e66, "ундециллион"),
+    (1e72, "дуодециллион"),
+    (1e78, "тредециллион"),
+    (1e84, "кваттордециллион"),
+    (1e90, "квиндециллион"),
+    (1e96, "сексдециллион"),
+    (1e102, "септендециллион"),
+    (1e108, "октодециллион"),
+    (1e114, "новемдециллион"),
+    (1e120, "вигинтиллион"),
+])
+
+
+_ORDINAL_BASE_RU = {
+    1: 'первый',
+    2: 'второй',
+    3: 'третий',
+    4: 'четвёртый',
+    5: 'пятый',
+    6: 'шестой',
+    7: 'седьмой',
+    8: 'восьмой',
+    9: 'девятый',
+    10: 'десятый',
+    11: 'одиннадцатый',
+    12: 'двенадцатый',
+    13: 'тринадцатый',
+    14: 'четырнадцатый',
+    15: 'пятнадцатый',
+    16: 'шестнадцатый',
+    17: 'семнадцатый',
+    18: 'восемнадцатый',
+    19: 'девятнадцатый',
+    20: 'двадцатый',
+    30: 'тридцатый',
+    40: "сороковой",
+    50: "пятидесятый",
+    60: "шестидесятый",
+    70: "семидесятый",
+    80: "восьмидесятый",
+    90: "девяностый",
+    1e2: "сотый",
+    2e2: "двухсотый",
+    3e2: "трёхсотый",
+    4e2: "четырёхсотый",
+    5e2: "пятисотый",
+    6e2: "шестисотый",
+    7e2: "семисотый",
+    8e2: "восьмисотый",
+    9e2: "девятисотый",
+    1e3: "тысячный"
+}
+
+
+_SHORT_ORDINAL_RU = {
+    1e6: "миллион",
+    1e9: "миллиард",
+    1e12: "триллион",
+    1e15: "квадриллион",
+    1e18: "квинтиллион",
+    1e21: "секстиллион",
+    1e24: "септиллион",
+    1e27: "октиллион",
+    1e30: "нониллион",
+    1e33: "дециллион",
+    1e36: "ундециллион",
+    1e39: "дуодециллион",
+    1e42: "тредециллион",
+    1e45: "кваттордециллион",
+    1e48: "квиндециллион",
+    1e51: "сексдециллион",
+    1e54: "септендециллион",
+    1e57: "октодециллион",
+    1e60: "новемдециллион",
+    1e63: "вигинтиллион"
+}
+_SHORT_ORDINAL_RU.update(_ORDINAL_BASE_RU)
+
+
+_LONG_ORDINAL_RU = {
+    1e6: "миллион",
+    1e9: "миллиард",
+    1e12: "биллион",
+    1e15: "биллиард",
+    1e18: "триллион",
+    1e21: "триллиард",
+    1e24: "квадриллион",
+    1e27: "квадриллиард",
+    1e30: "квинтиллион",
+    1e33: "квинтиллиард",
+    1e36: "секстиллион",
+    1e39: "секстиллиард",
+    1e42: "септиллион",
+    1e45: "септиллиард",
+    1e48: "октиллион",
+    1e51: "октиллиард",
+    1e54: "нониллион",
+    1e57: "нониллиард",
+    1e60: "дециллион",
+    1e63: "дециллиард",
+    1e66: "ундециллион",
+    1e72: "дуодециллион",
+    1e78: "тредециллион",
+    1e84: "кваттордециллион",
+    1e90: "квиндециллион",
+    1e96: "сексдециллион",
+    1e102: "септендециллион",
+    1e108: "октодециллион",
+    1e114: "новемдециллион",
+    1e120: "вигинтиллион"
+}
+_LONG_ORDINAL_RU.update(_ORDINAL_BASE_RU)
+
+# Months
+
+_MONTHS_CONVERSION = {
+    0: "january",
+    1: "february",
+    2: "march",
+    3: "april",
+    4: "may",
+    5: "june",
+    6: "july",
+    7: "august",
+    8: "september",
+    9: "october",
+    10: "november",
+    11: "december"
+}
+
+_MONTHS_RU = ['январь', 'февраль', 'март', 'апрель', 'май', 'июнь',
+             'июль', 'август', 'сентябрь', 'октябрь', 'ноябрь',
+             'декабрь']
+
+# Time
+_TIME_UNITS_CONVERSION = {
+    'микросекунд': 'microseconds',
+    'милисекунд': 'milliseconds',
+    'секунда': 'seconds',
+    'секунды': 'seconds',
+    'секунд': 'seconds',
+    'минута': 'minutes',
+    'минуты': 'minutes',
+    'минут': 'minutes',
+    'час': 'hours',
+    'часа': 'hours',
+    'часов': 'hours',
+    'день': 'days',
+    'дня': 'days',
+    'дней': 'days',
+    'неделя': 'weeks',
+    'недели': 'weeks',
+    'недель': 'weeks'
+}
diff --git a/lingua_franca/lang/format_ru.py b/lingua_franca/lang/format_ru.py
new file mode 100644
index 00000000..e87a9259
--- /dev/null
+++ b/lingua_franca/lang/format_ru.py
@@ -0,0 +1,474 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2017 Mycroft AI Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from lingua_franca.lang.format_common import convert_to_mixed_fraction
+from lingua_franca.lang.common_data_ru import _NUM_STRING_RU, \
+    _FRACTION_STRING_RU, _LONG_SCALE_RU, _SHORT_SCALE_RU, _SHORT_ORDINAL_RU, _LONG_ORDINAL_RU
+from lingua_franca.internal import FunctionNotLocalizedError
+
+
+def nice_number_ru(number, speech=True, denominators=range(1, 21)):
+    """ English helper for nice_number
+
+    This function formats a float to human understandable functions. Like
+    4.5 becomes "4 and a half" for speech and "4 1/2" for text
+
+    Args:
+        number (int or float): the float to format
+        speech (bool): format for speech (True) or display (False)
+        denominators (iter of ints): denominators to use, default [1 .. 20]
+    Returns:
+        (str): The formatted string.
+    """
+
+    result = convert_to_mixed_fraction(number, denominators)
+    if not result:
+        # Give up, just represent as a 3 decimal number
+        return str(round(number, 3))
+
+    whole, num, den = result
+
+    if not speech:
+        if num == 0:
+            # TODO: Number grouping?  E.g. "1,000,000"
+            return str(whole)
+        else:
+            return '{} {}/{}'.format(whole, num, den)
+
+    if num == 0:
+        return str(whole)
+    den_str = _FRACTION_STRING_RU[den]
+    if whole == 0:
+        if num == 1 and den <= 4:
+            return_string = '{}'.format(den_str)
+        else:
+            return_string = '{} {}'.format(num, den_str)
+    elif num == 1 and den == 2:
+        return_string = '{} с половиной'.format(whole)
+    else:
+        return_string = '{} и {} {}'.format(whole, num, den_str)
+    if 2 <= den <= 4:
+        if 2 <= num <= 4:
+            return_string = return_string[:-1] + 'и'
+        elif num > 4:
+            return_string = return_string[:-1] + 'ей'
+    elif den >= 5:
+        if 2 <= num <= 4:
+            return_string = return_string[:-2] + 'ые'
+        elif num > 4:
+            return_string = return_string[:-2] + 'ых'
+
+    return return_string
+
+
+def pronounce_number_ru(number, places=2, short_scale=True, scientific=False,
+                        ordinals=False):
+    """
+    Convert a number to it's spoken equivalent
+
+    For example, '5.2' would return 'five point two'
+
+    Args:
+        number(float or int): the number to pronounce (under 100)
+        places(int): maximum decimal places to speak
+        short_scale (bool) : use short (True) or long scale (False)
+            https://en.wikipedia.org/wiki/Names_of_large_numbers
+        scientific (bool): pronounce in scientific notation
+        ordinals (bool): pronounce in ordinal form "first" instead of "one"
+    Returns:
+        (str): The pronounced number
+    """
+    num = number
+    # deal with infinity
+    if num == float("inf"):
+        return "бесконечность"
+    elif num == float("-inf"):
+        return "минус бесконечность"
+    if scientific:
+        number = '%E' % num
+        n, power = number.replace("+", "").split("E")
+        power = int(power)
+        if power != 0:
+            if ordinals:
+                # This handles negative powers separately from the normal
+                # handling since each call disables the scientific flag
+                return '{}{} на десять в {}{} степени'.format(
+                    'минус ' if float(n) < 0 else '',
+                    pronounce_number_ru(
+                        abs(float(n)), places, short_scale, False, ordinals=True),
+                    'минус ' if power < 0 else '',
+                    pronounce_number_ru(abs(power), places, short_scale, False, ordinals=True))
+            else:
+                # This handles negative powers separately from the normal
+                # handling since each call disables the scientific flag
+                return '{}{} на десять в степени {}{}'.format(
+                    'минус ' if float(n) < 0 else '',
+                    pronounce_number_ru(
+                        abs(float(n)), places, short_scale, False, ordinals=False),
+                    'минус ' if power < 0 else '',
+                    pronounce_number_ru(abs(power), places, short_scale, False, ordinals=False))
+
+    if short_scale:
+        number_names = _NUM_STRING_RU.copy()
+        number_names.update(_SHORT_SCALE_RU)
+    else:
+        number_names = _NUM_STRING_RU.copy()
+        number_names.update(_LONG_SCALE_RU)
+
+    digits = [number_names[n] for n in range(0, 20)]
+
+    tens = [number_names[n] for n in range(10, 100, 10)]
+
+    if short_scale:
+        hundreds = [_SHORT_SCALE_RU[n] for n in _SHORT_SCALE_RU.keys()]
+    else:
+        hundreds = [_LONG_SCALE_RU[n] for n in _LONG_SCALE_RU.keys()]
+
+    # deal with negative numbers
+    result = ""
+    if num < 0:
+        result = "минус "
+    num = abs(num)
+
+    # check for a direct match
+    if num in number_names and not ordinals:
+        result += number_names[num]
+    else:
+        def _sub_thousand(n, ordinals=False):
+            assert 0 <= n <= 999
+            if n in _SHORT_ORDINAL_RU and ordinals:
+                return _SHORT_ORDINAL_RU[n]
+            if n <= 19:
+                return digits[n]
+            elif n <= 99:
+                q, r = divmod(n, 10)
+                return tens[q - 1] + (" " + _sub_thousand(r, ordinals) if r
+                                      else "")
+            else:
+                q, r = divmod(n, 100)
+                return _NUM_STRING_RU[q * 100] + (" " + _sub_thousand(r, ordinals) if r else "")
+
+        def _short_scale(n):
+            if n > max(_SHORT_SCALE_RU.keys()):
+                return "бесконечность"
+            ordi = ordinals
+
+            if int(n) != n:
+                ordi = False
+            n = int(n)
+            assert 0 <= n
+            res = []
+            for i, z in enumerate(_split_by(n, 1000)):
+                if not z:
+                    continue
+                number = _sub_thousand(z, not i and ordi)
+
+                if i:
+                    if i >= len(hundreds):
+                        return ""
+                    if ordi:
+                        if i * 1000 in _SHORT_ORDINAL_RU:
+                            if z == 1:
+                                number = _SHORT_ORDINAL_RU[i * 1000]
+                            else:
+                                if z > 5:
+                                    number = number[:-1] + "и"
+                                number += _SHORT_ORDINAL_RU[i * 1000]
+                        else:
+                            if n not in _SHORT_SCALE_RU:
+                                num = int("1" + "0" * (len(str(n)) // 3 * 3))
+
+                                if number[-3:] == "два":
+                                    number = number[:-1] + "ух"
+                                elif number[-2:] == "ри" or number[-2:] == "ре":
+                                    number = number[:-1] + "ёх"
+                                elif number[-1:] == "ь":
+                                    number = number[:-1] + "и"
+
+                                number += _SHORT_SCALE_RU[num] + "ный"
+                            else:
+                                number = _SHORT_SCALE_RU[n] + "ный"
+                    elif z == 1:
+                        number = hundreds[i - 1]
+                    else:
+                        if i == 1:
+                            if z % 10 == 1 and z % 100 // 10 != 1:
+                                number = number[:-2] + "на"
+                            elif z % 10 == 2 and z % 100 // 10 != 1:
+                                number = number[:-1] + "е"
+                            number += " " + plural_ru(z, "тысяча", "тысячи", "тысяч")
+                        elif 1 <= z % 10 <= 4 and z % 100 // 10 != 1:
+                            number += " " + hundreds[i - 1] + "а"
+                        else:
+                            number += " " + hundreds[i - 1] + "ов"
+
+                res.append(number)
+                ordi = False
+
+            return " ".join(reversed(res))
+
+        def _split_by(n, split=1000):
+            assert 0 <= n
+            res = []
+            while n:
+                n, r = divmod(n, split)
+                res.append(r)
+            return res
+
+        def _long_scale(n):
+            if n >= max(_LONG_SCALE_RU.keys()):
+                return "бесконечность"
+            ordi = ordinals
+            if int(n) != n:
+                ordi = False
+            n = int(n)
+            assert 0 <= n
+            res = []
+            for i, z in enumerate(_split_by(n, 1000000)):
+                if not z:
+                    continue
+                number = pronounce_number_ru(z, places, True, scientific,
+                                             ordinals=ordi and not i)
+                # strip off the comma after the thousand
+                if i:
+                    if i >= len(hundreds):
+                        return ""
+                    # plus one as we skip 'thousand'
+                    # (and 'hundred', but this is excluded by index value)
+                    number = number.replace(',', '')
+
+                    if ordi:
+                        if (i + 1) * 1000000 in _LONG_ORDINAL_RU:
+                            if z == 1:
+                                number = _LONG_ORDINAL_RU[
+                                    (i + 1) * 1000000]
+                            else:
+                                number += _LONG_ORDINAL_RU[
+                                    (i + 1) * 1000000]
+                        else:
+                            if n not in _LONG_SCALE_RU:
+                                num = int("1" + "0" * (len(str(n)) // 3 * 3))
+
+                                if number[-3:] == "два":
+                                    number = number[:-1] + "ух"
+                                elif number[-2:] == "ри" or number[-2:] == "ре":
+                                    number = number[:-1] + "ёх"
+                                elif number[-1:] == "ь":
+                                    number = number[:-1] + "и"
+
+                                number += _LONG_SCALE_RU[num] + "ный"
+                            else:
+                                number = " " + _LONG_SCALE_RU[n] + "ный"
+                    elif z == 1:
+                        number = hundreds[i]
+                    elif z <= 4:
+                        number += " " + hundreds[i] + "а"
+                    else:
+                        number += " " + hundreds[i] + "ов"
+
+                res.append(number)
+            return " ".join(reversed(res))
+
+        if short_scale:
+            result += _short_scale(num)
+        else:
+            result += _long_scale(num)
+
+    # deal with scientific notation unpronounceable as number
+    if not result and "e" in str(num):
+        return pronounce_number_ru(num, places, short_scale, scientific=True)
+    # Deal with fractional part
+    elif not num == int(num) and places > 0:
+        if abs(num) < 1.0 and (result == "минус " or not result):
+            result += "ноль"
+        result += " точка"
+        _num_str = str(num)
+        _num_str = _num_str.split(".")[1][0:places]
+        for char in _num_str:
+            result += " " + number_names[int(char)]
+    return result
+
+
+def nice_time_ru(dt, speech=True, use_24hour=True, use_ampm=False):
+    """
+    Format a time to a comfortable human format
+    For example, generate 'five thirty' for speech or '5:30' for
+    text display.
+    Args:
+        dt (datetime): date to format (assumes already in local timezone)
+        speech (bool): format for speech (default/True) or display (False)=Fal
+        use_24hour (bool): output in 24-hour/military or 12-hour format
+        use_ampm (bool): include the am/pm for 12-hour format
+    Returns:
+        (str): The formatted time string
+    """
+    if use_24hour:
+        # e.g. "03:01" or "14:22"
+        string = dt.strftime("%H:%M")
+    else:
+        if use_ampm:
+            # e.g. "3:01 AM" or "2:22 PM"
+            string = dt.strftime("%I:%M")
+            if dt.hour < 4:
+                string += " ночи"
+            elif dt.hour < 12:
+                string += " утра"
+            elif dt.hour < 18:
+                string += " дня"
+            else:
+                string += " вечера"
+        else:
+            # e.g. "3:01" or "2:22"
+            string = dt.strftime("%I:%M")
+        if string[0] == '0':
+            string = string[1:]  # strip leading zeros
+
+    if not speech:
+        return string
+
+    # Generate a speakable version of the time
+    if use_24hour:
+        speak = ""
+
+        # Either "0 8 hundred" or "13 hundred"
+        if string[0] == '0':
+            speak += pronounce_hour_ru(int(string[0])) + " "
+            speak += pronounce_number_ru(int(string[1]))
+        else:
+            speak = pronounce_hour_ru(int(string[0:2]))
+
+        speak += " "
+        if string[3:5] == '00':
+            speak += "ровно"
+        else:
+            if string[3] == '0':
+                speak += pronounce_number_ru(0) + " "
+                speak += pronounce_number_ru(int(string[4]))
+            else:
+                speak += pronounce_number_ru(int(string[3:5]))
+        return speak
+    else:
+        if dt.hour == 0 and dt.minute == 0:
+            return "полночь"
+        elif dt.hour == 12 and dt.minute == 0:
+            return "полдень"
+
+        hour = dt.hour % 12 or 12  # 12 hour clock and 0 is spoken as 12
+        if dt.minute == 15:
+            speak = pronounce_hour_ru(hour) + " с четвертью"
+        elif dt.minute == 30:
+            speak = pronounce_hour_ru(hour) + " с половиной"
+        elif dt.minute == 45:
+            next_hour = (dt.hour + 1) % 12 or 12
+            speak = "без четверти " + pronounce_hour_ru(next_hour)
+        else:
+            speak = pronounce_hour_ru(hour)
+
+            if dt.minute == 0:
+                if not use_ampm:
+                    if dt.hour % 12 == 1:
+                        return speak
+                    return speak + " " + plural_ru(dt.hour % 12, "час", "часа", "часов")
+            else:
+                if dt.minute < 10:
+                    speak += " ноль"
+                speak += " " + pronounce_number_ru(dt.minute)
+
+        if use_ampm:
+            if dt.hour < 4:
+                speak += " ночи"
+            elif dt.hour < 12:
+                speak += " утра"
+            elif dt.hour < 18:
+                speak += " дня"
+            else:
+                speak += " вечера"
+
+        return speak
+
+
+def nice_duration_ru(duration, speech=True):
+    """ Convert duration to a nice spoken timespan
+
+    Args:
+        seconds: number of seconds
+        minutes: number of minutes
+        hours: number of hours
+        days: number of days
+    Returns:
+        str: timespan as a string
+    """
+
+    if not speech:
+        raise FunctionNotLocalizedError
+
+    days = int(duration // 86400)
+    hours = int(duration // 3600 % 24)
+    minutes = int(duration // 60 % 60)
+    seconds = int(duration % 60)
+
+    out = ''
+
+    if days > 0:
+        out += pronounce_number_ru(days)
+        out += " " + plural_ru(days, "день", "дня", "дней")
+    if hours > 0:
+        if out:
+            out += " "
+        out += pronounce_number_ru(hours)
+        out += " " + plural_ru(hours, "час", "часа", "часов")
+    if minutes > 0:
+        if out:
+            out += " "
+        out += pronounce_number_feminine_ru(minutes)
+        out += " " + plural_ru(minutes, "минута", "минуты", "минут")
+    if seconds > 0:
+        if out:
+            out += " "
+        out += pronounce_number_feminine_ru(seconds)
+        out += " " + plural_ru(seconds, "секунда", "секунды", "секунд")
+
+    return out
+
+
+def pronounce_hour_ru(num):
+    if num == 1:
+        return "час"
+    return pronounce_number_ru(num)
+
+
+def pronounce_number_feminine_ru(num):
+    pronounced = pronounce_number_ru(num)
+
+    num %= 100
+    if num % 10 == 1 and num // 10 != 1:
+        return pronounced[:-2] + "на"
+    elif num % 10 == 2 and num // 10 != 1:
+        return pronounced[:-1] + "е"
+
+    return pronounced
+
+
+def plural_ru(num: int, one: str, few: str, many: str):
+    num %= 100
+    if num // 10 == 1:
+        return many
+    if num % 10 == 1:
+        return one
+    if 2 <= num % 10 <= 4:
+        return few
+    return many
diff --git a/lingua_franca/lang/parse_ru.py b/lingua_franca/lang/parse_ru.py
new file mode 100644
index 00000000..cd041ec7
--- /dev/null
+++ b/lingua_franca/lang/parse_ru.py
@@ -0,0 +1,1685 @@
+#
+# Copyright 2017 Mycroft AI Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+from datetime import datetime, timedelta
+
+from dateutil.relativedelta import relativedelta
+
+from lingua_franca.lang.parse_common import is_numeric, look_for_fractions, \
+    invert_dict, ReplaceableNumber, partition_list, tokenize, Token, Normalizer
+from lingua_franca.lang.common_data_ru import _NUM_STRING_RU, \
+    _LONG_ORDINAL_RU, _LONG_SCALE_RU, _SHORT_SCALE_RU, _SHORT_ORDINAL_RU, \
+    _FRACTION_STRING_RU, _MONTHS_CONVERSION, _MONTHS_RU, _TIME_UNITS_CONVERSION, \
+    _ORDINAL_BASE_RU
+
+import re
+import json
+from lingua_franca import resolve_resource_file
+from lingua_franca.time import now_local
+
+
+def generate_plurals_ru(originals):
+    """
+    Return a new set or dict containing the plural form of the original values,
+
+    In English this means all with 's' appended to them.
+
+    Args:
+        originals set(str) or dict(str, any): values to pluralize
+
+    Returns:
+        set(str) or dict(str, any)
+
+    """
+    suffixes = ["а", "ах", "ам", "ами", "ные", "ный", "ов", "ом", "ы"]
+    if isinstance(originals, dict):
+        return {key + suffix: value for key, value in originals.items() for suffix in suffixes}
+    return {value + suffix for value in originals for suffix in suffixes}
+
+
+# negate next number (-2 = 0 - 2)
+_NEGATIVES = {"минус"}
+
+# sum the next number (twenty two = 20 + 2)
+_SUMS = {'двадцать', '20', 'тридцать', '30', 'сорок', '40', 'пятьдесят', '50',
+         'шестьдесят', '60', 'семьдесят', '70', 'восемьдесят', '80', 'девяносто', '90',
+         'сто', '100', 'двести', '200', 'триста', '300', 'четыреста', '400',
+         'пятьсот', '500', 'шестьсот', '600', 'семьсот', '700', 'восемьсот', '800',
+         'девятьсот', '900'}
+
+_MULTIPLIES_LONG_SCALE_RU = set(_LONG_SCALE_RU.values()) | \
+                            generate_plurals_ru(_LONG_SCALE_RU.values())
+
+_MULTIPLIES_SHORT_SCALE_RU = set(_SHORT_SCALE_RU.values()) | \
+                             generate_plurals_ru(_SHORT_SCALE_RU.values())
+
+# split sentence parse separately and sum ( 2 and a half = 2 + 0.5 )
+_FRACTION_MARKER = {"и", "с", " "}
+
+# decimal marker ( 1 point 5 = 1 + 0.5)
+_DECIMAL_MARKER = {"целая", "целых", "точка", "запятая"}
+
+_STRING_NUM_RU = invert_dict(_NUM_STRING_RU)
+_STRING_NUM_RU.update({
+    "тысяч": 1e3,
+})
+_STRING_NUM_RU.update(generate_plurals_ru(_STRING_NUM_RU))
+_STRING_NUM_RU.update({
+    "четверти": 0.25,
+    "четвёртая": 0.25,
+    "четвёртых": 0.25,
+    "третья": 1 / 3,
+    "третяя": 1 / 3,
+    "вторая": 0.5,
+    "вторых": 0.5,
+    "половина": 0.5,
+    "половиной": 0.5,
+    "пол": 0.5,
+    "одна": 1,
+    "двойка": 2,
+    "двое": 2,
+    "пара": 2,
+    "сот": 100,
+    "сотен": 100,
+    "сотни": 100,
+    "сотня": 100,
+})
+
+_WORDS_NEXT_RU = [
+    "будущая", "будущее", "будущей", "будущий", "будущим", "будущую",
+    "новая", "новое", "новой", "новый", "новым",
+    "следующая", "следующее", "следующей", "следующем", "следующий", "следующую",
+]
+_WORDS_PREV_RU = [
+    "предыдущая", "предыдущем", "предыдущей", "предыдущий", "предыдущим", "предыдущую",
+    "прошедшая", "прошедшем", "прошедшей", "прошедший", "прошедшим", "прошедшую",
+    "прошлая", "прошлой", "прошлом", "прошлую", "прошлый", "прошлым",
+    "том", "тот",
+]
+_WORDS_CURRENT_RU = [
+    "данная", "данное", "данном", "данный",
+    "настойщая", "настоящее", "настойщем", "настойщем", "настойщий",
+    "нынешняя", "нынешнее", "нынешней", "нынешнем", "нынешний",
+    "текущая", "текущее", "текущей", "текущем", "текущий",
+    "это", "этим", "этой", "этом", "этот", "эту",
+]
+_WORDS_NOW_RU = [
+    "теперь",
+    "сейчас",
+]
+_WORDS_MORNING_RU = ["утро", "утром"]
+_WORDS_DAY_RU = ["днём"]
+_WORDS_EVENING_RU = ["вечер", "вечером"]
+_WORDS_NIGHT_RU = ["ночь", "ночью"]
+
+_STRING_SHORT_ORDINAL_RU = invert_dict(_SHORT_ORDINAL_RU)
+_STRING_LONG_ORDINAL_RU = invert_dict(_LONG_ORDINAL_RU)
+
+
+def _convert_words_to_numbers_ru(text, short_scale=True, ordinals=False):
+    """
+    Convert words in a string into their equivalent numbers.
+    Args:
+        text str:
+        short_scale boolean: True if short scale numbers should be used.
+        ordinals boolean: True if ordinals (e.g. first, second, third) should
+                          be parsed to their number values (1, 2, 3...)
+
+    Returns:
+        str
+        The original text, with numbers subbed in where appropriate.
+
+    """
+    text = text.lower()
+    tokens = tokenize(text)
+    numbers_to_replace = \
+        _extract_numbers_with_text_ru(tokens, short_scale, ordinals)
+    numbers_to_replace.sort(key=lambda number: number.start_index)
+
+    results = []
+    for token in tokens:
+        if not numbers_to_replace or \
+                token.index < numbers_to_replace[0].start_index:
+            results.append(token.word)
+        else:
+            if numbers_to_replace and \
+                    token.index == numbers_to_replace[0].start_index:
+                results.append(str(numbers_to_replace[0].value))
+            if numbers_to_replace and \
+                    token.index == numbers_to_replace[0].end_index:
+                numbers_to_replace.pop(0)
+
+    return ' '.join(results)
+
+
+def _extract_numbers_with_text_ru(tokens, short_scale=True,
+                                  ordinals=False, fractional_numbers=True):
+    """
+    Extract all numbers from a list of Tokens, with the words that
+    represent them.
+
+    Args:
+        [Token]: The tokens to parse.
+        short_scale bool: True if short scale numbers should be used, False for
+                          long scale. True by default.
+        ordinals bool: True if ordinal words (first, second, third, etc) should
+                       be parsed.
+        fractional_numbers bool: True if we should look for fractions and
+                                 decimals.
+
+    Returns:
+        [ReplaceableNumber]: A list of tuples, each containing a number and a
+                         string.
+
+    """
+    placeholder = "<placeholder>"  # inserted to maintain correct indices
+    results = []
+    while True:
+        to_replace = \
+            _extract_number_with_text_ru(tokens, short_scale,
+                                         ordinals, fractional_numbers)
+
+        if not to_replace:
+            break
+
+        results.append(to_replace)
+
+        tokens = [
+            t if not
+            to_replace.start_index <= t.index <= to_replace.end_index
+            else
+            Token(placeholder, t.index) for t in tokens
+        ]
+    results.sort(key=lambda n: n.start_index)
+    return results
+
+
+def _extract_number_with_text_ru(tokens, short_scale=True,
+                                 ordinals=False, fractional_numbers=True):
+    """
+    This function extracts a number from a list of Tokens.
+
+    Args:
+        tokens str: the string to normalize
+        short_scale (bool): use short scale if True, long scale if False
+        ordinals (bool): consider ordinal numbers, third=3 instead of 1/3
+        fractional_numbers (bool): True if we should look for fractions and
+                                   decimals.
+    Returns:
+        ReplaceableNumber
+
+    """
+    number, tokens = \
+        _extract_number_with_text_ru_helper(tokens, short_scale,
+                                            ordinals, fractional_numbers)
+    return ReplaceableNumber(number, tokens)
+
+
+def _extract_number_with_text_ru_helper(tokens,
+                                        short_scale=True, ordinals=False,
+                                        fractional_numbers=True):
+    """
+    Helper for _extract_number_with_text_en.
+
+    This contains the real logic for parsing, but produces
+    a result that needs a little cleaning (specific, it may
+    contain leading articles that can be trimmed off).
+
+    Args:
+        tokens [Token]:
+        short_scale boolean:
+        ordinals boolean:
+        fractional_numbers boolean:
+
+    Returns:
+        int or float, [Tokens]
+
+    """
+    if fractional_numbers:
+        fraction, fraction_text = \
+            _extract_fraction_with_text_ru(tokens, short_scale, ordinals)
+        if fraction:
+            return fraction, fraction_text
+
+        decimal, decimal_text = \
+            _extract_decimal_with_text_ru(tokens, short_scale, ordinals)
+        if decimal:
+            return decimal, decimal_text
+
+    return _extract_whole_number_with_text_ru(tokens, short_scale, ordinals)
+
+
+def _extract_fraction_with_text_ru(tokens, short_scale, ordinals):
+    """
+    Extract fraction numbers from a string.
+
+    This function handles text such as '2 and 3/4'. Note that "one half" or
+    similar will be parsed by the whole number function.
+
+    Args:
+        tokens [Token]: words and their indexes in the original string.
+        short_scale boolean:
+        ordinals boolean:
+
+    Returns:
+        (int or float, [Token])
+        The value found, and the list of relevant tokens.
+        (None, None) if no fraction value is found.
+
+    """
+    for c in _FRACTION_MARKER:
+        partitions = partition_list(tokens, lambda t: t.word == c)
+
+        if len(partitions) == 3:
+            numbers1 = \
+                _extract_numbers_with_text_ru(partitions[0], short_scale,
+                                              ordinals, fractional_numbers=False)
+            numbers2 = \
+                _extract_numbers_with_text_ru(partitions[2], short_scale,
+                                              ordinals, fractional_numbers=True)
+
+            if not numbers1 or not numbers2:
+                return None, None
+
+            # ensure first is not a fraction and second is a fraction
+            num1 = numbers1[-1]
+            num2 = numbers2[0]
+            if num1.value >= 1 and 0 < num2.value < 1:
+                return num1.value + num2.value, \
+                       num1.tokens + partitions[1] + num2.tokens
+
+    return None, None
+
+
+def _extract_decimal_with_text_ru(tokens, short_scale, ordinals):
+    """
+    Extract decimal numbers from a string.
+
+    This function handles text such as '2 point 5'.
+
+    Notes:
+        While this is a helper for extract_number_xx, it also depends on
+        extract_number_xx, to parse out the components of the decimal.
+
+        This does not currently handle things like:
+            number dot number number number
+
+    Args:
+        tokens [Token]: The text to parse.
+        short_scale boolean:
+        ordinals boolean:
+
+    Returns:
+        (float, [Token])
+        The value found and relevant tokens.
+        (None, None) if no decimal value is found.
+
+    """
+    for c in _DECIMAL_MARKER:
+        partitions = partition_list(tokens, lambda t: t.word == c)
+
+        if len(partitions) == 3:
+            numbers1 = \
+                _extract_numbers_with_text_ru(partitions[0], short_scale,
+                                              ordinals, fractional_numbers=False)
+            numbers2 = \
+                _extract_numbers_with_text_ru(partitions[2], short_scale,
+                                              ordinals, fractional_numbers=False)
+
+            if not numbers1 or not numbers2:
+                return None, None
+
+            number = numbers1[-1]
+            decimal = numbers2[0]
+
+            # TODO handle number dot number number number
+            if "." not in str(decimal.text):
+                return number.value + float('0.' + str(decimal.value)), \
+                       number.tokens + partitions[1] + decimal.tokens
+    return None, None
+
+
+def _extract_whole_number_with_text_ru(tokens, short_scale, ordinals):
+    """
+    Handle numbers not handled by the decimal or fraction functions. This is
+    generally whole numbers. Note that phrases such as "one half" will be
+    handled by this function, while "one and a half" are handled by the
+    fraction function.
+
+    Args:
+        tokens [Token]:
+        short_scale boolean:
+        ordinals boolean:
+
+    Returns:
+        int or float, [Tokens]
+        The value parsed, and tokens that it corresponds to.
+
+    """
+    multiplies, string_num_ordinal, string_num_scale = \
+        _initialize_number_data(short_scale)
+
+    number_words = []  # type: [Token]
+    val = False
+    prev_val = None
+    next_val = None
+    to_sum = []
+    for idx, token in enumerate(tokens):
+        current_val = None
+        if next_val:
+            next_val = None
+            continue
+
+        word = token.word
+        if word in word in _NEGATIVES:
+            number_words.append(token)
+            continue
+
+        prev_word = tokens[idx - 1].word if idx > 0 else ""
+        next_word = tokens[idx + 1].word if idx + 1 < len(tokens) else ""
+
+        # In Russian (?) we do no use suffix (1st,2nd,..) but use point instead (1.,2.,..)
+        if is_numeric(word[:-1]) and \
+                (word.endswith(".")):
+            # explicit ordinals, 1st, 2nd, 3rd, 4th.... Nth
+            word = word[:-1]
+
+            # handle nth one
+        #    if next_word == "one":
+            # would return 1 instead otherwise
+        #        tokens[idx + 1] = Token("", idx)
+        #        next_word = ""
+
+        # Normalize Russian inflection of numbers (один, одна, одно,...)
+        if not ordinals:
+            word = _text_ru_inflection_normalize(word, 1)
+
+        if word not in string_num_scale and \
+                word not in _STRING_NUM_RU and \
+                word not in _SUMS and \
+                word not in multiplies and \
+                not (ordinals and word in string_num_ordinal) and \
+                not is_numeric(word) and \
+                not is_fractional_ru(word, short_scale=short_scale) and \
+                not look_for_fractions(word.split('/')):
+            words_only = [token.word for token in number_words]
+            if number_words and not all([w in _NEGATIVES for w in words_only]):
+                break
+            else:
+                number_words = []
+                continue
+        elif word not in multiplies \
+                and prev_word not in multiplies \
+                and prev_word not in _SUMS \
+                and not (ordinals and prev_word in string_num_ordinal) \
+                and prev_word not in _NEGATIVES:
+            number_words = [token]
+        elif prev_word in _SUMS and word in _SUMS:
+            number_words = [token]
+        else:
+            number_words.append(token)
+
+        # is this word already a number ?
+        if is_numeric(word):
+            if word.isdigit():  # doesn't work with decimals
+                val = int(word)
+            else:
+                val = float(word)
+            current_val = val
+
+        # is this word the name of a number ?
+        if word in _STRING_NUM_RU:
+            val = _STRING_NUM_RU.get(word)
+            current_val = val
+        elif word in string_num_scale:
+            val = string_num_scale.get(word)
+            current_val = val
+        elif ordinals and word in string_num_ordinal:
+            val = string_num_ordinal[word]
+            current_val = val
+
+        # is the prev word an ordinal number and current word is one?
+        # second one, third one
+        if ordinals and prev_word in string_num_ordinal and val == 1:
+            val = prev_val
+
+        # is the prev word a number and should we sum it?
+        # twenty two, fifty six
+        if (prev_word in _SUMS and val and val < 10) \
+                or (prev_word in _SUMS and val and val < 100 and prev_val >= 100) \
+                or all([prev_word in multiplies, val < prev_val if prev_val else False]):
+            val = prev_val + val
+
+        # is the prev word a number and should we multiply it?
+        # twenty hundred, six hundred
+        if word in multiplies:
+            if not prev_val:
+                prev_val = 1
+            val = prev_val * val
+
+        # is this a spoken fraction?
+        # half cup
+        if val is False:
+            val = is_fractional_ru(word, short_scale=short_scale)
+            current_val = val
+
+        # 2 fifths
+        if not ordinals:
+            next_val = is_fractional_ru(next_word, short_scale=short_scale)
+            if next_val:
+                if not val:
+                    val = 1
+                val = val * next_val
+                number_words.append(tokens[idx + 1])
+
+        # is this a negative number?
+        if val and prev_word and prev_word in _NEGATIVES:
+            val = 0 - val
+
+        # let's make sure it isn't a fraction
+        if not val:
+            # look for fractions like "2/3"
+            a_pieces = word.split('/')
+            if look_for_fractions(a_pieces):
+                val = float(a_pieces[0]) / float(a_pieces[1])
+        else:
+            if all([
+                prev_word in _SUMS,
+                word not in _SUMS,
+                word not in multiplies,
+                current_val >= 10
+            ]):
+                # Backtrack - we've got numbers we can't sum.
+                number_words.pop()
+                val = prev_val
+                break
+            prev_val = val
+
+            if word in multiplies and next_word not in multiplies:
+                # handle long numbers
+                # six hundred sixty six
+                # two million five hundred thousand
+                #
+                # This logic is somewhat complex, and warrants
+                # extensive documentation for the next coder's sake.
+                #
+                # The current word is a power of ten. `current_val` is
+                # its integer value. `val` is our working sum
+                # (above, when `current_val` is 1 million, `val` is
+                # 2 million.)
+                #
+                # We have a dict `string_num_scale` containing [value, word]
+                # pairs for "all" powers of ten: string_num_scale[10] == "ten.
+                #
+                # We need go over the rest of the tokens, looking for other
+                # powers of ten. If we find one, we compare it with the current
+                # value, to see if it's smaller than the current power of ten.
+                #
+                # Numbers which are not powers of ten will be passed over.
+                #
+                # If all the remaining powers of ten are smaller than our
+                # current value, we can set the current value aside for later,
+                # and begin extracting another portion of our final result.
+                # For example, suppose we have the following string.
+                # The current word is "million".`val` is 9000000.
+                # `current_val` is 1000000.
+                #
+                #    "nine **million** nine *hundred* seven **thousand**
+                #     six *hundred* fifty seven"
+                #
+                # Iterating over the rest of the string, the current
+                # value is larger than all remaining powers of ten.
+                #
+                # The if statement passes, and nine million (9000000)
+                # is appended to `to_sum`.
+                #
+                # The main variables are reset, and the main loop begins
+                # assembling another number, which will also be appended
+                # under the same conditions.
+                #
+                # By the end of the main loop, to_sum will be a list of each
+                # "place" from 100 up: [9000000, 907000, 600]
+                #
+                # The final three digits will be added to the sum of that list
+                # at the end of the main loop, to produce the extracted number:
+                #
+                #    sum([9000000, 907000, 600]) + 57
+                # == 9,000,000 + 907,000 + 600 + 57
+                # == 9,907,657
+                #
+                # >>> foo = "nine million nine hundred seven thousand six
+                #            hundred fifty seven"
+                # >>> extract_number(foo)
+                # 9907657
+
+                time_to_sum = True
+                for other_token in tokens[idx + 1:]:
+                    if other_token.word in multiplies:
+                        if string_num_scale[other_token.word] >= current_val:
+                            time_to_sum = False
+                        else:
+                            continue
+                    if not time_to_sum:
+                        break
+                if time_to_sum:
+                    to_sum.append(val)
+                    val = 0
+                    prev_val = 0
+
+    if val is not None and to_sum:
+        val += sum(to_sum)
+
+    return val, number_words
+
+
+def _initialize_number_data(short_scale):
+    """
+    Generate dictionaries of words to numbers, based on scale.
+
+    This is a helper function for _extract_whole_number.
+
+    Args:
+        short_scale boolean:
+
+    Returns:
+        (set(str), dict(str, number), dict(str, number))
+        multiplies, string_num_ordinal, string_num_scale
+
+    """
+    multiplies = _MULTIPLIES_SHORT_SCALE_RU if short_scale \
+        else _MULTIPLIES_LONG_SCALE_RU
+
+    string_num_ordinal_ru = _STRING_SHORT_ORDINAL_RU if short_scale \
+        else _STRING_LONG_ORDINAL_RU
+
+    string_num_scale_ru = _SHORT_SCALE_RU if short_scale else _LONG_SCALE_RU
+    string_num_scale_ru = invert_dict(string_num_scale_ru)
+    string_num_scale_ru.update(generate_plurals_ru(string_num_scale_ru))
+    return multiplies, string_num_ordinal_ru, string_num_scale_ru
+
+
+def extract_number_ru(text, short_scale=True, ordinals=False):
+    """
+    This function extracts a number from a text string,
+    handles pronunciations in long scale and short scale
+
+    https://en.wikipedia.org/wiki/Names_of_large_numbers
+
+    Args:
+        text (str): the string to normalize
+        short_scale (bool): use short scale if True, long scale if False
+        ordinals (bool): consider ordinal numbers, third=3 instead of 1/3
+    Returns:
+        (int) or (float) or False: The extracted number or False if no number
+                                   was found
+
+    """
+    return _extract_number_with_text_ru(tokenize(text.lower()),
+                                        short_scale, ordinals).value
+
+
+def extract_duration_ru(text):
+    """
+    Convert an english phrase into a number of seconds
+
+    Convert things like:
+        "10 minute"
+        "2 and a half hours"
+        "3 days 8 hours 10 minutes and 49 seconds"
+    into an int, representing the total number of seconds.
+
+    The words used in the duration will be consumed, and
+    the remainder returned.
+
+    As an example, "set a timer for 5 minutes" would return
+    (300, "set a timer for").
+
+    Args:
+        text (str): string containing a duration
+
+    Returns:
+        (timedelta, str):
+                    A tuple containing the duration and the remaining text
+                    not consumed in the parsing. The first value will
+                    be None if no duration is found. The text returned
+                    will have whitespace stripped from the ends.
+    """
+    if not text:
+        return None
+
+    # Russian inflection for time: минута, минуты, минут - safe to use минута as pattern
+    # For day: день, дня, дней - short pattern not applicable, list all
+
+    time_units = {
+        'microseconds': 0,
+        'milliseconds': 0,
+        'seconds': 0,
+        'minutes': 0,
+        'hours': 0,
+        'days': 0,
+        'weeks': 0
+    }
+
+    pattern = r"(?P<value>\d+(?:\.?\d+)?)(?:\s+|\-){unit}(?:а|ов|у|ут|уту)?"
+    text = _convert_words_to_numbers_ru(text)
+
+    for (unit_ru, unit_en) in _TIME_UNITS_CONVERSION.items():
+        unit_pattern = pattern.format(unit=unit_ru)
+
+        def repl(match):
+            time_units[unit_en] += float(match.group(1))
+            return ''
+
+        text = re.sub(unit_pattern, repl, text)
+
+    text = text.strip()
+    duration = timedelta(**time_units) if any(time_units.values()) else None
+
+    return duration, text
+
+
+def extract_datetime_ru(text, anchor_date=None, default_time=None):
+    """ Convert a human date reference into an exact datetime
+
+    Convert things like
+        "today"
+        "tomorrow afternoon"
+        "next Tuesday at 4pm"
+        "August 3rd"
+    into a datetime.  If a reference date is not provided, the current
+    local time is used.  Also consumes the words used to define the date
+    returning the remaining string.  For example, the string
+       "what is Tuesday's weather forecast"
+    returns the date for the forthcoming Tuesday relative to the reference
+    date and the remainder string
+       "what is weather forecast".
+
+    The "next" instance of a day or weekend is considered to be no earlier than
+    48 hours in the future. On Friday, "next Monday" would be in 3 days.
+    On Saturday, "next Monday" would be in 9 days.
+
+    Args:
+        text (str): string containing date words
+        anchor_date (datetime): A reference date/time for "tommorrow", etc
+        default_time (time): Time to set if no time was found in the string
+
+    Returns:
+        [datetime, str]: An array containing the datetime and the remaining
+                         text not consumed in the parsing, or None if no
+                         date or time related text was found.
+    """
+
+    def clean_string(s):
+        # clean unneeded punctuation and capitalization among other things.
+        # Normalize Russian inflection
+        s = s.lower().replace('?', '').replace('.', '').replace(',', '') \
+            .replace("сегодня вечером", "вечером") \
+            .replace("сегодня ночью", "ночью")
+        word_list = s.split()
+
+        for idx, word in enumerate(word_list):
+            # word = word.replace("'s", "")
+            ##########
+            # Russian Day Ordinals - we do not use 1st,2nd format
+            #    instead we use full ordinal number names with specific format(suffix)
+            #   Example: тридцать первого > 31
+            count_ordinals = 0
+            if word == "первого":
+                count_ordinals = 1  # These two have different format
+            elif word == "третьего":
+                count_ordinals = 3
+            elif word.endswith("ого"):
+                tmp = word[:-3]
+                tmp += "ый"
+                for nr, name in _ORDINAL_BASE_RU.items():
+                    if name == tmp:
+                        count_ordinals = nr
+
+            # If number is bigger than 19 check if next word is also ordinal
+            #  and count them together
+            if count_ordinals > 19:
+                if word_list[idx + 1] == "первого":
+                    count_ordinals += 1  # These two have different format
+                elif word_list[idx + 1] == "третьего":
+                    count_ordinals += 3
+                elif word_list[idx + 1].endswith("ого"):
+                    tmp = word_list[idx + 1][:-3]
+                    tmp += "ый"
+                    for nr, name in _ORDINAL_BASE_RU.items():
+                        if name == tmp and nr < 10:
+                            # write only if sum makes acceptable count of days in month
+                            if (count_ordinals + nr) <= 31:
+                                count_ordinals += nr
+
+            if count_ordinals > 0:
+                word = str(count_ordinals)  # Write normalized value into word
+            if count_ordinals > 20:
+                # If counted number is greater than 20, clear next word so it is not used again
+                word_list[idx + 1] = ""
+            ##########
+            # Remove inflection from Russian months
+
+            word_list[idx] = word
+
+        return word_list
+
+    def date_found():
+        return found or \
+               (
+                       date_string != "" or
+                       year_offset != 0 or month_offset != 0 or
+                       day_offset is True or hr_offset != 0 or
+                       hr_abs or min_offset != 0 or
+                       min_abs or sec_offset != 0
+               )
+
+    if text == "":
+        return None
+
+    anchor_date = anchor_date or now_local()
+    found = False
+    day_specified = False
+    day_offset = False
+    month_offset = 0
+    year_offset = 0
+    today = anchor_date.strftime("%w")
+    current_year = anchor_date.strftime("%Y")
+    from_flag = False
+    date_string = ""
+    has_year = False
+    time_qualifier = ""
+
+    time_qualifiers_am = _WORDS_MORNING_RU
+    time_qualifiers_pm = ['дня', 'вечера']
+    time_qualifiers_pm.extend(_WORDS_DAY_RU)
+    time_qualifiers_pm.extend(_WORDS_EVENING_RU)
+    time_qualifiers_pm.extend(_WORDS_NIGHT_RU)
+    time_qualifiers_list = set(time_qualifiers_am + time_qualifiers_pm)
+    markers = ['на', 'в', 'во', 'до', 'на', 'это',
+               'около', 'этот', 'через', 'спустя', 'за', 'тот']
+    days = ['понедельник', 'вторник', 'среда',
+            'четверг', 'пятница', 'суббота', 'воскресенье']
+    months = _MONTHS_RU
+    recur_markers = days + ['выходные', 'викенд']
+    months_short = ['янв', 'фев', 'мар', 'апр', 'май', 'июн', 'июл', 'авг',
+                    'сен', 'окт', 'ноя', 'дек']
+    year_multiples = ["десятилетие", "век", "тысячелетие"]
+
+    words = clean_string(text)
+    preposition = ""
+
+    for idx, word in enumerate(words):
+        if word == "":
+            continue
+
+        if word in markers:
+            preposition = word
+
+        word = _text_ru_inflection_normalize(word, 2)
+        word_prev_prev = _text_ru_inflection_normalize(
+            words[idx - 2], 2) if idx > 1 else ""
+        word_prev = _text_ru_inflection_normalize(
+            words[idx - 1], 2) if idx > 0 else ""
+        word_next = _text_ru_inflection_normalize(
+            words[idx + 1], 2) if idx + 1 < len(words) else ""
+        word_next_next = _text_ru_inflection_normalize(
+            words[idx + 2], 2) if idx + 2 < len(words) else ""
+
+        # this isn't in clean string because I don't want to save back to words
+        start = idx
+        used = 0
+        if word in _WORDS_NOW_RU and not date_string:
+            result_str = " ".join(words[idx + 1:])
+            result_str = ' '.join(result_str.split())
+            extracted_date = anchor_date.replace(microsecond=0)
+            return [extracted_date, result_str]
+        elif word_next in year_multiples:
+            multiplier = None
+            if is_numeric(word):
+                multiplier = extract_number_ru(word)
+            multiplier = multiplier or 1
+            multiplier = int(multiplier)
+            used += 2
+            if word_next == "десятилетие":
+                year_offset = multiplier * 10
+            elif word_next == "век":
+                year_offset = multiplier * 100
+            elif word_next == "тысячелетие":
+                year_offset = multiplier * 1000
+        elif word in time_qualifiers_list and preposition != "через" and word_next != "назад":
+            time_qualifier = word
+        # parse today, tomorrow, day after tomorrow
+        elif word == "сегодня" and not from_flag:
+            day_offset = 0
+            used += 1
+        elif word == "завтра" and not from_flag:
+            day_offset = 1
+            used += 1
+        elif word == "послезавтра" and not from_flag:
+            day_offset = 2
+            used += 1
+        elif word == "после" and word_next == "завтра" and not from_flag:
+            day_offset = 2
+            used += 2
+        elif word == "позавчера" and not from_flag:
+            day_offset = -2
+            used += 1
+        elif word == "вчера" and not from_flag:
+            day_offset = -1
+            used += 1
+        elif (word in ["день", "дня"] and
+              word_next == "после" and
+              word_next_next == "завтра" and
+              not from_flag and
+              (not word_prev or not word_prev[0].isdigit())):
+            day_offset = 2
+            used = 2
+        elif word in ["день", "дня"] and is_numeric(word_prev) and preposition == "через":
+            if word_prev and word_prev[0].isdigit():
+                day_offset += int(word_prev)
+                start -= 1
+                used = 2
+        elif word in ["день", "дня"] and is_numeric(word_prev) and word_next == "назад":
+            if word_prev and word_prev[0].isdigit():
+                day_offset += -int(word_prev)
+                start -= 1
+                used = 3
+        elif word == "сегодня" and not from_flag and word_prev:
+            if word_prev[0].isdigit():
+                day_offset += int(word_prev) * 7
+                start -= 1
+                used = 2
+            elif word_prev in _WORDS_NEXT_RU:
+                day_offset = 7
+                start -= 1
+                used = 2
+            elif word_prev in _WORDS_PREV_RU:
+                day_offset = -7
+                start -= 1
+                used = 2
+                # parse 10 months, next month, last month
+        elif word == "неделя" and not from_flag and preposition in ["через", "на"]:
+            if word_prev[0].isdigit():
+                day_offset = int(word_prev) * 7
+                start -= 1
+                used = 2
+            elif word_prev in _WORDS_NEXT_RU:
+                day_offset = 7
+                start -= 1
+                used = 2
+            elif word_prev in _WORDS_PREV_RU:
+                day_offset = -7
+                start -= 1
+                used = 2
+        elif word == "месяц" and not from_flag and preposition in ["через", "на"]:
+            if word_prev[0].isdigit():
+                month_offset = int(word_prev)
+                start -= 1
+                used = 2
+            elif word_prev in _WORDS_NEXT_RU:
+                month_offset = 1
+                start -= 1
+                used = 2
+            elif word_prev in _WORDS_PREV_RU:
+                month_offset = -1
+                start -= 1
+                used = 2
+        # parse 5 years, next year, last year
+        elif word == "год" and not from_flag and preposition in ["через", "на"]:
+            if word_prev[0].isdigit():
+                year_offset = int(word_prev)
+                start -= 1
+                used = 2
+            elif word_prev in _WORDS_NEXT_RU:
+                year_offset = 1
+                start -= 1
+                used = 2
+            elif word_prev in _WORDS_PREV_RU:
+                year_offset = -1
+                start -= 1
+                used = 2
+            elif word_prev == "через":
+                year_offset = 1
+                used = 1
+        # parse Monday, Tuesday, etc., and next Monday,
+        # last Tuesday, etc.
+        elif word in days and not from_flag:
+            d = days.index(word)
+            day_offset = (d + 1) - int(today)
+            used = 1
+            if day_offset < 0:
+                day_offset += 7
+            if word_prev in _WORDS_NEXT_RU:
+                if day_offset <= 2:
+                    day_offset += 7
+                used += 1
+                start -= 1
+            elif word_prev in _WORDS_PREV_RU:
+                day_offset -= 7
+                used += 1
+                start -= 1
+        elif word in months or word in months_short and not from_flag:
+            try:
+                m = months.index(word)
+            except ValueError:
+                m = months_short.index(word)
+            used += 1
+            # Convert Russian months to english
+            date_string = _MONTHS_CONVERSION.get(m)
+            if word_prev and (word_prev[0].isdigit() or
+                              (word_prev == " " and word_prev_prev[0].isdigit())):
+                if word_prev == " " and word_prev_prev[0].isdigit():
+                    date_string += " " + words[idx - 2]
+                    used += 1
+                    start -= 1
+                else:
+                    date_string += " " + word_prev
+                start -= 1
+                used += 1
+                if word_next and word_next[0].isdigit():
+                    date_string += " " + word_next
+                    used += 1
+                    has_year = True
+                else:
+                    has_year = False
+
+            elif word_next and word_next[0].isdigit():
+                date_string += " " + word_next
+                used += 1
+                if word_next_next and word_next_next[0].isdigit():
+                    date_string += " " + word_next_next
+                    used += 1
+                    has_year = True
+                else:
+                    has_year = False
+
+        # parse 5 days from tomorrow, 10 weeks from next thursday,
+        # 2 months from July
+        valid_followups = days + months + months_short
+        valid_followups.append("сегодня")
+        valid_followups.append("завтра")
+        valid_followups.append("послезавтра")
+        valid_followups.append("вчера")
+        valid_followups.append("позавчера")
+        for followup in _WORDS_NEXT_RU:
+            valid_followups.append(followup)
+        for followup in _WORDS_PREV_RU:
+            valid_followups.append(followup)
+        for followup in _WORDS_CURRENT_RU:
+            valid_followups.append(followup)
+        for followup in _WORDS_NOW_RU:
+            valid_followups.append(followup)
+        if (word in ["до", "по", "от", "с", "со"]) and word_next in valid_followups:
+            used = 2
+            from_flag = True
+            if word_next == "завтра":
+                day_offset += 1
+            elif word_next == "послезавтра":
+                day_offset += 2
+            elif word_next == "вчера":
+                day_offset -= 1
+            elif word_next == "позавчера":
+                day_offset -= 2
+            elif word_next in days:
+                d = days.index(word_next)
+                tmp_offset = (d + 1) - int(today)
+                used = 2
+                if tmp_offset < 0:
+                    tmp_offset += 7
+                day_offset += tmp_offset
+            elif word_next_next and word_next_next in days:
+                d = days.index(word_next_next)
+                tmp_offset = (d + 1) - int(today)
+                used = 3
+                if word_next in _WORDS_NEXT_RU:
+                    if day_offset <= 2:
+                        tmp_offset += 7
+                    used += 1
+                    start -= 1
+                elif word_next in _WORDS_PREV_RU:
+                    tmp_offset -= 7
+                    used += 1
+                    start -= 1
+                day_offset += tmp_offset
+        if used > 0:
+            if start - 1 > 0 and (words[start - 1] in _WORDS_CURRENT_RU):
+                start -= 1
+                used += 1
+
+            for i in range(0, used):
+                words[i + start] = ""
+
+            if start - 1 >= 0 and words[start - 1] in markers:
+                words[start - 1] = ""
+            found = True
+            day_specified = True
+
+    # parse time
+    hr_offset = 0
+    min_offset = 0
+    sec_offset = 0
+    hr_abs = None
+    min_abs = None
+    military = False
+    preposition = ""
+
+    for idx, word in enumerate(words):
+        if word == "":
+            continue
+
+        if word in markers:
+            preposition = word
+
+        word = _text_ru_inflection_normalize(word, 2)
+        word_prev_prev = _text_ru_inflection_normalize(
+            words[idx - 2], 2) if idx > 1 else ""
+        word_prev = _text_ru_inflection_normalize(
+            words[idx - 1], 2) if idx > 0 else ""
+        word_next = _text_ru_inflection_normalize(
+            words[idx + 1], 2) if idx + 1 < len(words) else ""
+        word_next_next = _text_ru_inflection_normalize(
+            words[idx + 2], 2) if idx + 2 < len(words) else ""
+
+        # parse noon, midnight, morning, afternoon, evening
+        used = 0
+        if word == "полдень":
+            hr_abs = 12
+            used += 1
+        elif word == "полночь":
+            hr_abs = 0
+            used += 1
+        elif word in _WORDS_MORNING_RU:
+            if hr_abs is None:
+                hr_abs = 8
+            used += 1
+        elif word in _WORDS_DAY_RU:
+            if hr_abs is None:
+                hr_abs = 15
+            used += 1
+        elif word in _WORDS_EVENING_RU:
+            if hr_abs is None:
+                hr_abs = 19
+            used += 1
+            if word_next != "" and word_next[0].isdigit() and ":" in word_next:
+                used -= 1
+        elif word in _WORDS_NIGHT_RU:
+            if hr_abs is None:
+                hr_abs = 22
+        # parse half an hour, quarter hour
+        elif word == "час" and \
+                (word_prev in markers or word_prev_prev in markers):
+            if word_prev in ["пол", "половина"]:
+                min_offset = 30
+            elif word_prev == "четверть":
+                min_offset = 15
+            elif word_prev == "через":
+                hr_offset = 1
+            else:
+                hr_offset = 1
+            if word_prev_prev in markers:
+                words[idx - 2] = ""
+                if word_prev_prev in _WORDS_CURRENT_RU:
+                    day_specified = True
+            words[idx - 1] = ""
+            used += 1
+            hr_abs = -1
+            min_abs = -1
+            # parse 5:00 am, 12:00 p.m., etc
+        # parse in a minute
+        elif word == "минута" and word_prev == "через":
+            min_offset = 1
+            words[idx - 1] = ""
+            used += 1
+        # parse in a second
+        elif word == "секунда" and word_prev == "через":
+            sec_offset = 1
+            words[idx - 1] = ""
+            used += 1
+        elif word[0].isdigit():
+            is_time = True
+            str_hh = ""
+            str_mm = ""
+            remainder = ""
+            word_next_next_next = words[idx + 3] \
+                if idx + 3 < len(words) else ""
+            if word_next in _WORDS_EVENING_RU or word_next in _WORDS_NIGHT_RU or word_next_next in _WORDS_EVENING_RU \
+                    or word_next_next in _WORDS_NIGHT_RU or word_prev in _WORDS_EVENING_RU \
+                    or word_prev in _WORDS_NIGHT_RU or word_prev_prev in _WORDS_EVENING_RU \
+                    or word_prev_prev in _WORDS_NIGHT_RU or word_next_next_next in _WORDS_EVENING_RU \
+                    or word_next_next_next in _WORDS_NIGHT_RU:
+                remainder = "pm"
+                used += 1
+                if word_prev in _WORDS_EVENING_RU or word_prev in _WORDS_NIGHT_RU:
+                    words[idx - 1] = ""
+                if word_prev_prev in _WORDS_EVENING_RU or word_prev_prev in _WORDS_NIGHT_RU:
+                    words[idx - 2] = ""
+                if word_next_next in _WORDS_EVENING_RU or word_next_next in _WORDS_NIGHT_RU:
+                    used += 1
+                if word_next_next_next in _WORDS_EVENING_RU or word_next_next_next in _WORDS_NIGHT_RU:
+                    used += 1
+
+            if ':' in word:
+                # parse colons
+                # "3:00 in the morning"
+                stage = 0
+                length = len(word)
+                for i in range(length):
+                    if stage == 0:
+                        if word[i].isdigit():
+                            str_hh += word[i]
+                        elif word[i] == ":":
+                            stage = 1
+                        else:
+                            stage = 2
+                            i -= 1
+                    elif stage == 1:
+                        if word[i].isdigit():
+                            str_mm += word[i]
+                        else:
+                            stage = 2
+                            i -= 1
+                    elif stage == 2:
+                        remainder = word[i:].replace(".", "")
+                        break
+                if remainder == "":
+                    next_word = word_next.replace(".", "")
+                    if next_word in ["am", "pm", "ночи", "утра", "дня", "вечера"]:
+                        remainder = next_word
+                        used += 1
+                    elif next_word == "часа" and word_next_next in ["am", "pm", "ночи", "утра", "дня", "вечера"]:
+                        remainder = word_next_next
+                        used += 2
+                    elif word_next in _WORDS_MORNING_RU:
+                        remainder = "am"
+                        used += 2
+                    elif word_next in _WORDS_DAY_RU:
+                        remainder = "pm"
+                        used += 2
+                    elif word_next in _WORDS_EVENING_RU:
+                        remainder = "pm"
+                        used += 2
+                    elif word_next == "этого" and word_next_next in _WORDS_MORNING_RU:
+                        remainder = "am"
+                        used = 2
+                        day_specified = True
+                    elif word_next == "на" and word_next_next in _WORDS_DAY_RU:
+                        remainder = "pm"
+                        used = 2
+                        day_specified = True
+                    elif word_next == "на" and word_next_next in _WORDS_EVENING_RU:
+                        remainder = "pm"
+                        used = 2
+                        day_specified = True
+                    elif word_next == "в" and word_next_next in _WORDS_NIGHT_RU:
+                        if str_hh and int(str_hh) > 5:
+                            remainder = "pm"
+                        else:
+                            remainder = "am"
+                        used += 2
+                    elif hr_abs and hr_abs != -1:
+                        if hr_abs >= 12:
+                            remainder = "pm"
+                        else:
+                            remainder = "am"
+                        used += 1
+                    else:
+                        if time_qualifier != "":
+                            military = True
+                            if str_hh and int(str_hh) <= 12 and \
+                                    (time_qualifier in time_qualifiers_pm):
+                                str_hh += str(int(str_hh) + 12)
+
+            else:
+                # try to parse numbers without colons
+                # 5 hours, 10 minutes etc.
+                length = len(word)
+                str_num = ""
+                remainder = ""
+                for i in range(length):
+                    if word[i].isdigit():
+                        str_num += word[i]
+                    else:
+                        remainder += word[i]
+
+                if remainder == "":
+                    remainder = word_next.replace(".", "").lstrip().rstrip()
+                if (
+                        remainder == "pm" or
+                        word_next == "pm" or
+                        remainder == "p.m." or
+                        word_next == "p.m." or
+                        (remainder == "дня" and preposition != 'через') or
+                        (word_next == "дня" and preposition != 'через') or
+                        remainder == "вечера" or
+                        word_next == "вечера"):
+                    str_hh = str_num
+                    remainder = "pm"
+                    used = 1
+                    if (
+                            remainder == "pm" or
+                            word_next == "pm" or
+                            remainder == "p.m." or
+                            word_next == "p.m." or
+                            (remainder == "дня" and preposition != 'через') or
+                            (word_next == "дня" and preposition != 'через') or
+                            remainder == "вечера" or
+                            word_next == "вечера"):
+                        str_hh = str_num
+                        remainder = "pm"
+                        used = 1
+                elif (
+                        remainder == "am" or
+                        word_next == "am" or
+                        remainder == "a.m." or
+                        word_next == "a.m." or
+                        remainder == "ночи" or
+                        word_next == "ночи" or
+                        remainder == "утра" or
+                        word_next == "утра"):
+                    str_hh = str_num
+                    remainder = "am"
+                    used = 1
+                elif (
+                        remainder in recur_markers or
+                        word_next in recur_markers or
+                        word_next_next in recur_markers):
+                    # Ex: "7 on mondays" or "3 this friday"
+                    # Set str_hh so that is_time == True
+                    # when am or pm is not specified
+                    str_hh = str_num
+                    used = 1
+                else:
+                    if int(str_num) > 100:
+                        str_hh = str(int(str_num) // 100)
+                        str_mm = str(int(str_num) % 100)
+                        military = True
+                        if word_next == "час":
+                            used += 1
+                    elif (
+                            (word_next == "час" or
+                             remainder == "час") and
+                            word[0] != '0' and
+                            # (wordPrev != "в" and wordPrev != "на")
+                            word_prev == "через"
+                            and
+                            (
+                                    int(str_num) < 100 or
+                                    int(str_num) > 2400
+                            )):
+                        # ignores military time
+                        # "in 3 hours"
+                        hr_offset = int(str_num)
+                        used = 2
+                        is_time = False
+                        hr_abs = -1
+                        min_abs = -1
+                    elif word_next == "минута" or \
+                            remainder == "минута":
+                        # "in 10 minutes"
+                        min_offset = int(str_num)
+                        used = 2
+                        is_time = False
+                        hr_abs = -1
+                        min_abs = -1
+                    elif word_next == "секунда" \
+                            or remainder == "секунда":
+                        # in 5 seconds
+                        sec_offset = int(str_num)
+                        used = 2
+                        is_time = False
+                        hr_abs = -1
+                        min_abs = -1
+                    elif int(str_num) > 100:
+                        # military time, eg. "3300 hours"
+                        str_hh = str(int(str_num) // 100)
+                        str_mm = str(int(str_num) % 100)
+                        military = True
+                        if word_next == "час" or \
+                                remainder == "час":
+                            used += 1
+                    elif word_next and word_next[0].isdigit():
+                        # military time, e.g. "04 38 hours"
+                        str_hh = str_num
+                        str_mm = word_next
+                        military = True
+                        used += 1
+                        if (word_next_next == "час" or
+                                remainder == "час"):
+                            used += 1
+                    elif (
+                            word_next == "" or word_next == "час" or
+                            (
+                                    (word_next == "в" or word_next == "на") and
+                                    (
+                                            word_next_next == time_qualifier
+                                    )
+                            ) or word_next in _WORDS_EVENING_RU or
+                            word_next_next in _WORDS_EVENING_RU):
+
+                        str_hh = str_num
+                        str_mm = "00"
+                        if word_next == "час":
+                            used += 1
+                        if (word_next == "в" or word_next == "на"
+                                or word_next_next == "в" or word_next_next == "на"):
+                            used += (1 if (word_next ==
+                                           "в" or word_next == "на") else 2)
+                            word_next_next_next = words[idx + 3] \
+                                if idx + 3 < len(words) else ""
+
+                            if (word_next_next and
+                                    (word_next_next in time_qualifier or
+                                     word_next_next_next in time_qualifier)):
+                                if (word_next_next in time_qualifiers_pm or
+                                        word_next_next_next in time_qualifiers_pm):
+                                    remainder = "pm"
+                                    used += 1
+                                if (word_next_next in time_qualifiers_am or
+                                        word_next_next_next in time_qualifiers_am):
+                                    remainder = "am"
+                                    used += 1
+
+                        if time_qualifier != "":
+                            if time_qualifier in time_qualifiers_pm:
+                                remainder = "pm"
+                                used += 1
+
+                            elif time_qualifier in time_qualifiers_am:
+                                remainder = "am"
+                                used += 1
+                            else:
+                                # TODO: Unsure if this is 100% accurate
+                                used += 1
+                                military = True
+                        elif remainder == "час":
+                            if word_next_next in ["ночи", "утра"]:
+                                remainder = "am"
+                                used += 1
+                            elif word_next_next in ["дня", "вечера"]:
+                                remainder = "pm"
+                                used += 1
+                            else:
+                                remainder = ""
+
+                    else:
+                        is_time = False
+            hh = int(str_hh) if str_hh else 0
+            mm = int(str_mm) if str_mm else 0
+            hh = hh + 12 if remainder == "pm" and hh < 12 else hh
+            hh = hh - 12 if remainder == "am" and hh >= 12 else hh
+            if (not military and
+                    remainder not in ['am', 'pm', 'час', 'минута', 'секунда'] and
+                    ((not day_specified) or 0 <= day_offset < 1)):
+
+                # ambiguous time, detect whether they mean this evening or
+                # the next morning based on whether it has already passed
+                if anchor_date.hour < hh or (anchor_date.hour == hh and
+                                             anchor_date.minute < mm):
+                    pass  # No modification needed
+                elif anchor_date.hour < hh + 12:
+                    hh += 12
+                else:
+                    # has passed, assume the next morning
+                    day_offset += 1
+            if time_qualifier in time_qualifiers_pm and hh < 12:
+                hh += 12
+
+            if hh > 24 or mm > 59:
+                is_time = False
+                used = 0
+            if is_time:
+                hr_abs = hh
+                min_abs = mm
+                used += 1
+
+        if used > 0:
+            # removed parsed words from the sentence
+            for i in range(used):
+                if idx + i >= len(words):
+                    break
+                words[idx + i] = ""
+
+            # if wordPrev == "o" or wordPrev == "oh":
+            #    words[words.index(wordPrev)] = ""
+
+            if word_prev == "скоро":
+                hr_offset = -1
+                words[idx - 1] = ""
+                idx -= 1
+            elif word_prev == "позже":
+                hr_offset = 1
+                words[idx - 1] = ""
+                idx -= 1
+            if idx > 0 and word_prev in markers:
+                words[idx - 1] = ""
+                if word_prev in _WORDS_CURRENT_RU:
+                    day_specified = True
+            if idx > 1 and word_prev_prev in markers:
+                words[idx - 2] = ""
+                if word_prev_prev in _WORDS_CURRENT_RU:
+                    day_specified = True
+
+            idx += used - 1
+            found = True
+    # check that we found a date
+    if not date_found():
+        return None
+
+    if day_offset is False:
+        day_offset = 0
+
+    # perform date manipulation
+
+    extracted_date = anchor_date.replace(microsecond=0)
+    if date_string != "":
+        # date included an explicit date, e.g. "june 5" or "june 2, 2017"
+        try:
+            temp = datetime.strptime(date_string, "%B %d")
+        except ValueError:
+            # Try again, allowing the year
+            temp = datetime.strptime(date_string, "%B %d %Y")
+        extracted_date = extracted_date.replace(hour=0, minute=0, second=0)
+        if not has_year:
+            temp = temp.replace(year=extracted_date.year,
+                                tzinfo=extracted_date.tzinfo)
+            if extracted_date < temp:
+                extracted_date = extracted_date.replace(
+                    year=int(current_year),
+                    month=int(temp.strftime("%m")),
+                    day=int(temp.strftime("%d")),
+                    tzinfo=extracted_date.tzinfo)
+            else:
+                extracted_date = extracted_date.replace(
+                    year=int(current_year) + 1,
+                    month=int(temp.strftime("%m")),
+                    day=int(temp.strftime("%d")),
+                    tzinfo=extracted_date.tzinfo)
+        else:
+            extracted_date = extracted_date.replace(
+                year=int(temp.strftime("%Y")),
+                month=int(temp.strftime("%m")),
+                day=int(temp.strftime("%d")),
+                tzinfo=extracted_date.tzinfo)
+    else:
+        # ignore the current HH:MM:SS if relative using days or greater
+        if hr_offset == 0 and min_offset == 0 and sec_offset == 0:
+            extracted_date = extracted_date.replace(hour=0, minute=0, second=0)
+
+    if year_offset != 0:
+        extracted_date = extracted_date + relativedelta(years=year_offset)
+    if month_offset != 0:
+        extracted_date = extracted_date + relativedelta(months=month_offset)
+    if day_offset != 0:
+        extracted_date = extracted_date + relativedelta(days=day_offset)
+    if hr_abs != -1 and min_abs != -1:
+        # If no time was supplied in the string set the time to default
+        # time if it's available
+        if hr_abs is None and min_abs is None and default_time is not None:
+            hr_abs, min_abs = default_time.hour, default_time.minute
+        else:
+            hr_abs = hr_abs or 0
+            min_abs = min_abs or 0
+
+        extracted_date = extracted_date + relativedelta(hours=hr_abs,
+                                                        minutes=min_abs)
+        if (hr_abs != 0 or min_abs != 0) and date_string == "":
+            if not day_specified and anchor_date > extracted_date:
+                extracted_date = extracted_date + relativedelta(days=1)
+    if hr_offset != 0:
+        extracted_date = extracted_date + relativedelta(hours=hr_offset)
+    if min_offset != 0:
+        extracted_date = extracted_date + relativedelta(minutes=min_offset)
+    if sec_offset != 0:
+        extracted_date = extracted_date + relativedelta(seconds=sec_offset)
+    for idx, word in enumerate(words):
+        if words[idx] == "и" and \
+                words[idx - 1] == "" and words[idx + 1] == "":
+            words[idx] = ""
+
+    result_str = " ".join(words)
+    result_str = ' '.join(result_str.split())
+    return [extracted_date, result_str]
+
+
+def is_fractional_ru(input_str, short_scale=True):
+    """
+    This function takes the given text and checks if it is a fraction.
+
+    Args:
+        input_str (str): the string to check if fractional
+        short_scale (bool): use short scale if True, long scale if False
+    Returns:
+        (bool) or (float): False if not a fraction, otherwise the fraction
+
+    """
+    if input_str[-3:] in ["тые", "тых"]:  # leading number is bigger than one (две четвёртые, три пятых)
+        input_str = input_str[-3:] + "тая"
+    fractions = {"целая": 1}  # first four numbers have little different format
+
+    for num in _FRACTION_STRING_RU:  # Numbers from 2 to 1 hundred, more is not usually used in common speech
+        if num > 1:
+            fractions[_FRACTION_STRING_RU[num]] = num
+
+    if input_str.lower() in fractions:
+        return 1.0 / fractions[input_str.lower()]
+    return False
+
+
+def extract_numbers_ru(text, short_scale=True, ordinals=False):
+    """
+        Takes in a string and extracts a list of numbers.
+
+    Args:
+        text (str): the string to extract a number from
+        short_scale (bool): Use "short scale" or "long scale" for large
+            numbers -- over a million.  The default is short scale, which
+            is now common in most English speaking countries.
+            See https://en.wikipedia.org/wiki/Names_of_large_numbers
+        ordinals (bool): consider ordinal numbers, e.g. third=3 instead of 1/3
+    Returns:
+        list: list of extracted numbers as floats
+    """
+    results = _extract_numbers_with_text_ru(tokenize(text),
+                                            short_scale, ordinals)
+    return [float(result.value) for result in results]
+
+
+class RussianNormalizer(Normalizer):
+    with open(resolve_resource_file("text/ru-ru/normalize.json"), encoding='utf8') as f:
+        _default_config = json.load(f)
+
+
+def normalize_ru(text, remove_articles=True):
+    """ Russian string normalization """
+    return RussianNormalizer().normalize(text, remove_articles)
+
+
+def _text_ru_inflection_normalize(word, arg):
+    """
+    Russian Inflection normalizer.
+
+    This try to normalize known inflection. This function is called
+    from multiple places, each one is defined with arg.
+
+    Args:
+        word [Word]
+        arg [Int]
+
+    Returns:
+        word [Word]
+
+    """
+    if word in ["тысяч", "тысячи"]:
+        return "тысяча"
+
+    if arg == 1:  # _extract_whole_number_with_text_ru
+        if word in ["одна", "одним", "одно", "одной"]:
+            return "один"
+        if word == "две":
+            return "два"
+        if word == "пару":
+            return "пара"
+
+    elif arg == 2:  # extract_datetime_ru
+        if word in ["часа", "часам", "часами", "часов", "часу"]:
+            return "час"
+        if word in ["минут", "минутам", "минутами", "минуту", "минуты"]:
+            return "минута"
+        if word in ["секунд", "секундам", "секундами", "секунду", "секунды"]:
+            return "секунда"
+        if word in ["дней", "дни"]:
+            return "день"
+        if word in ["неделе", "недели", "недель"]:
+            return "неделя"
+        if word in ["месяца", "месяцев"]:
+            return "месяц"
+        if word in ["года", "лет"]:
+            return "год"
+        if word in _WORDS_MORNING_RU:
+            return "утром"
+        if word in ["полудне", "полудня"]:
+            return "полдень"
+        if word in _WORDS_EVENING_RU:
+            return "вечером"
+        if word in _WORDS_NIGHT_RU:
+            return "ночь"
+        if word in ["викенд", "выходным", "выходных"]:
+            return "выходные"
+        if word in ["столетие", "столетий", "столетия"]:
+            return "век"
+
+        # Week days
+        if word in ["среду", "среды"]:
+            return "среда"
+        if word in ["пятницу", "пятницы"]:
+            return "пятница"
+        if word in ["субботу", "субботы"]:
+            return "суббота"
+
+        # Months
+        if word in ["марта", "марте"]:
+            return "март"
+        if word in ["мае", "мая"]:
+            return "май"
+        if word in ["августа", "августе"]:
+            return "август"
+
+        if word[-2:] in ["ле", "ля", "не", "ня", "ре", "ря"]:
+            tmp = word[:-1] + "ь"
+            for name in _MONTHS_RU:
+                if name == tmp:
+                    return name
+
+    return word
diff --git a/lingua_franca/res/text/ru-ru/date_time.json b/lingua_franca/res/text/ru-ru/date_time.json
new file mode 100644
index 00000000..6c835473
--- /dev/null
+++ b/lingua_franca/res/text/ru-ru/date_time.json
@@ -0,0 +1,149 @@
+{
+  "decade_format": {
+    "1": {"match": "^\\d$", "format": "{x}"},
+    "2": {"match": "^1\\d$", "format": "{xx}"},
+    "3": {"match": "^\\d0$", "format": "{x0}"},
+    "4": {"match": "^[2-9]\\d$", "format": "{x0} {x}"},
+    "default": "{number}"
+  },
+  "hundreds_format": {
+    "1": {"match": "^1\\d{2}$", "format": "сто"},
+    "2": {"match": "^2\\d{2}$", "format": "двести"},
+    "3": {"match": "^[34]\\d{2}$", "format": "{x_in_x00}ста"},
+    "4": {"match": "^\\d{3}$", "format": "{x_in_x00}сот"},
+    "default": "{number}"
+  },
+  "thousand_format": {
+    "1": {"match": "^10\\d{2}$", "format": "тысяча"},
+    "2": {"match": "^11\\d{2}$", "format": "тысяча сто"},
+    "3": {"match": "^12\\d{2}$", "format": "тысяча двести"},
+    "4": {"match": "^1[34]\\d{2}$", "format": "тысяча {x_in_x00}ста"},
+    "5": {"match": "^1\\d{3}$", "format": "тысяча {x_in_x00}сот"},
+
+    "6": {"match": "^20\\d{2}$", "format": "две тысячи"},
+    "7": {"match": "^21\\d{2}$", "format": "две тысячи сто"},
+    "8": {"match": "^22\\d{2}$", "format": "две тысячи двести"},
+    "9": {"match": "^2[34]\\d{2}$", "format": "две тысячи {x_in_x00}ста"},
+    "10": {"match": "^2\\d{3}$", "format": "две тысячи {x_in_x00}сот"},
+
+    "11": {"match": "^[34]0\\d{2}$", "format": "{x_in_x000} тысячи"},
+    "12": {"match": "^[34]1\\d{2}$", "format": "{x_in_x000} тысячи сто"},
+    "13": {"match": "^[34]2\\d{2}$", "format": "{x_in_x000} тысячи двести"},
+    "14": {"match": "^[34][34]\\d{2}$", "format": "{x_in_x000} тысячи {x_in_x00}ста"},
+    "15": {"match": "^[34]\\d{3}$", "format": "{x_in_x000} тысячи {x_in_x00}сот"},
+
+    "16": {"match": "^[5-9]0\\d{2}$", "format": "{x_in_x000} тысяч"},
+    "17": {"match": "^[5-9]1\\d{2}$", "format": "{x_in_x000} тысяч сто"},
+    "18": {"match": "^[5-9]2\\d{2}$", "format": "{x_in_x000} тысяч двести"},
+    "19": {"match": "^[5-9][34]\\d{2}$", "format": "{x_in_x000} тысяч {x_in_x00}ста"},
+    "20": {"match": "^[5-9]\\d{3}$", "format": "{x_in_x000} тысяч {x_in_x00}сот"},
+
+    "default": "{number}"
+  },
+  "year_format": {
+    "1": {"match": "^\\d\\d?$", "format": "{formatted_decade} {bc}"},
+    "2": {"match": "^\\d00$", "format": "{formatted_hundreds} {bc}"},
+    "3": {"match": "^\\d{3}$", "format": "{formatted_hundreds} {formatted_decade} {bc}"},
+    "4": {"match": "^\\d{2}00$", "format": "{formatted_thousand} {bc}"},
+    "5": {"match": "^\\d{4}$", "format": "{formatted_thousand} {formatted_decade} {bc}"},
+    "default": "{year} {bc}",
+    "bc": "до нашей эры"
+  },
+  "date_format": {
+    "date_full": "{weekday}, {day} {month}, {formatted_year}",
+    "date_full_no_year": "{weekday}, {day} {month}",
+    "date_full_no_year_month": "{weekday}, {day}",
+    "today": "сегодня",
+    "tomorrow": "завтра",
+    "yesterday": "вчера"
+  },
+  "date_time_format": {
+    "date_time": "{formatted_date} в {formatted_time}"
+  },
+  "weekday": {
+    "0": "в понедельник",
+    "1": "во вторник",
+    "2": "в среду",
+    "3": "в четверг",
+    "4": "в пятницу",
+    "5": "в субботу",
+    "6": "в воскресенье"
+  },
+  "date": {
+    "1": "первого",
+    "2": "второго",
+    "3": "третьего",
+    "4": "четвёртого",
+    "5": "пятого",
+    "6": "шестого",
+    "7": "седьмого",
+    "8": "восьмого",
+    "9": "девятого",
+    "10": "десятого",
+    "11": "одиннадцатого",
+    "12": "двенадцатого",
+    "13": "тринадцатого",
+    "14": "четырнадцатого",
+    "15": "пятнадцатого",
+    "16": "шестнадцатого",
+    "17": "семнадцатого",
+    "18": "восемнадцатого",
+    "19": "девятнадцатого",
+    "20": "двадцатого",
+    "21": "двадцать первого",
+    "22": "двадцать второго",
+    "23": "двадцать третьего",
+    "24": "двадцать четвёртого",
+    "25": "двадцать пятого",
+    "26": "двадцать шестого",
+    "27": "двадцать седьмого",
+    "28": "двадцать восьмого",
+    "29": "двадцать девятого",
+    "30": "тридцатого",
+    "31": "тридцать первого"
+  },
+  "month": {
+    "1": "января",
+    "2": "февраля",
+    "3": "марта",
+    "4": "апреля",
+    "5": "мая",
+    "6": "июня",
+    "7": "июля",
+    "8": "августа",
+    "9": "сентября",
+    "10": "октября",
+    "11": "ноября",
+    "12": "декабря"
+  },
+  "number": {
+    "0": "ноль",
+    "1": "один",
+    "2": "два",
+    "3": "три",
+    "4": "четыре",
+    "5": "пять",
+    "6": "шесть",
+    "7": "семь",
+    "8": "восемь",
+    "9": "девять",
+    "10": "десять",
+    "11": "одиннадцать",
+    "12": "двенадцать",
+    "13": "тринадцать",
+    "14": "четырнадцать",
+    "15": "пятнадцать",
+    "16": "шестнадцать",
+    "17": "семнадцать",
+    "18": "восемнадцать",
+    "19": "девятнадцать",
+    "20": "двадцать",
+    "30": "тридцать",
+    "40": "сорок",
+    "50": "пятьдесят",
+    "60": "шестьдесят",
+    "70": "семьдесят",
+    "80": "восемьдесят",
+    "90": "девяносто"
+  }
+}
diff --git a/lingua_franca/res/text/ru-ru/days.word b/lingua_franca/res/text/ru-ru/days.word
index d72864ba..d170fff4 100644
--- a/lingua_franca/res/text/ru-ru/days.word
+++ b/lingua_franca/res/text/ru-ru/days.word
@@ -1,1 +1,1 @@
-дни
+дней
diff --git a/lingua_franca/res/text/ru-ru/hours.word b/lingua_franca/res/text/ru-ru/hours.word
index 785d4438..80c7bead 100644
--- a/lingua_franca/res/text/ru-ru/hours.word
+++ b/lingua_franca/res/text/ru-ru/hours.word
@@ -1,1 +1,1 @@
-часы
+часов
diff --git a/lingua_franca/res/text/ru-ru/minutes.word b/lingua_franca/res/text/ru-ru/minutes.word
index 9ba05503..00b261a4 100644
--- a/lingua_franca/res/text/ru-ru/minutes.word
+++ b/lingua_franca/res/text/ru-ru/minutes.word
@@ -1,1 +1,1 @@
-минуты
+минут
diff --git a/lingua_franca/res/text/ru-ru/normalize.json b/lingua_franca/res/text/ru-ru/normalize.json
new file mode 100644
index 00000000..49bb02b3
--- /dev/null
+++ b/lingua_franca/res/text/ru-ru/normalize.json
@@ -0,0 +1,46 @@
+{
+  "lowercase": false,
+  "numbers_to_digits": true,
+  "expand_contractions": true,
+  "remove_symbols": false,
+  "remove_accents": false,
+  "remove_articles": false,
+  "remove_stopwords": false,
+  "contractions": {},
+  "word_replacements": {},
+  "number_replacements": {
+    "ноль": "0",
+    "нуль": "0",
+    "один": "1",
+    "одна": "1",
+    "два": "2",
+    "две": "2",
+    "три": "3",
+    "четыре": "4",
+    "пять": "5",
+    "шесть": "6",
+    "семь": "7",
+    "восемь": "8",
+    "девять": "9",
+    "десять": "10",
+    "одиннадцать": "11",
+    "двенадцать": "12",
+    "тринадцать": "13",
+    "четырнадцать": "14",
+    "пятнадцать": "15",
+    "шестнадцать": "16",
+    "семнадцать": "17",
+    "восемнадцать": "18",
+    "девятнадцать": "19",
+    "двадцать": "20",
+    "тридцать": "30",
+    "сорок": "40",
+    "пятьдесят": "50",
+    "шестьдесят": "60",
+    "семьдесят": "70",
+    "восемьдесят": "80",
+    "девяносто": "90"
+  },
+  "stopwords": [],
+  "articles": []
+}
\ No newline at end of file
diff --git a/lingua_franca/res/text/ru-ru/second.word b/lingua_franca/res/text/ru-ru/second.word
index 669981f1..f1be0808 100644
--- a/lingua_franca/res/text/ru-ru/second.word
+++ b/lingua_franca/res/text/ru-ru/second.word
@@ -1,1 +1,1 @@
-секунду
+секунда
diff --git a/lingua_franca/res/text/ru-ru/seconds.word b/lingua_franca/res/text/ru-ru/seconds.word
index c59570db..ba1da0d7 100644
--- a/lingua_franca/res/text/ru-ru/seconds.word
+++ b/lingua_franca/res/text/ru-ru/seconds.word
@@ -1,1 +1,1 @@
-секунды
+секунд


Test patch:
diff --git a/lingua_franca/res/text/ru-ru/date_time_test.json b/lingua_franca/res/text/ru-ru/date_time_test.json
new file mode 100644
index 00000000..ac827863
--- /dev/null
+++ b/lingua_franca/res/text/ru-ru/date_time_test.json
@@ -0,0 +1,43 @@
+{
+  "test_nice_year": {
+    "1": {"datetime_param": "1, 1, 31, 13, 22, 3", "bc": "True", "assertEqual": "один до нашей эры" },
+    "2": {"datetime_param": "10, 1, 31, 13, 22, 3", "bc": "True", "assertEqual": "десять до нашей эры" },
+    "3": {"datetime_param": "92, 1, 31, 13, 22, 3", "bc": "True", "assertEqual": "девяносто два до нашей эры" },
+    "4": {"datetime_param": "803, 1, 31, 13, 22, 3", "bc": "None", "assertEqual": "восемьсот три" },
+    "5": {"datetime_param": "811, 1, 31, 13, 22, 3", "bc": "None", "assertEqual": "восемьсот одиннадцать" },
+    "6": {"datetime_param": "454, 1, 31, 13, 22, 3", "bc": "None", "assertEqual": "четыреста пятьдесят четыре" },
+    "7": {"datetime_param": "1005, 1, 31, 13, 22, 3", "bc": "False", "assertEqual": "тысяча пять" },
+    "8": {"datetime_param": "1012, 1, 31, 13, 22, 3", "bc": "False", "assertEqual": "тысяча двенадцать" },
+    "9": {"datetime_param": "1046, 1, 31, 13, 22, 3", "bc": "False", "assertEqual": "тысяча сорок шесть" },
+    "10": {"datetime_param": "1807, 1, 31, 13, 22, 3", "bc": "None", "assertEqual": "тысяча восемьсот семь" },
+    "11": {"datetime_param": "1717, 1, 31, 13, 22, 3", "bc": "None", "assertEqual": "тысяча семьсот семнадцать" },
+    "12": {"datetime_param": "1988, 1, 31, 13, 22, 3", "bc": "None", "assertEqual": "тысяча девятьсот восемьдесят восемь"},
+    "13": {"datetime_param": "2009, 1, 31, 13, 22, 3", "bc": "None", "assertEqual": "две тысячи девять"},
+    "14": {"datetime_param": "2018, 1, 31, 13, 22, 3", "bc": "None", "assertEqual": "две тысячи восемнадцать"},
+    "15": {"datetime_param": "2021, 1, 31, 13, 22, 3", "bc": "None", "assertEqual": "две тысячи двадцать один"},
+    "16": {"datetime_param": "2030, 1, 31, 13, 22, 3", "bc": "None", "assertEqual": "две тысячи тридцать"},
+    "17": {"datetime_param": "2100, 1, 31, 13, 22, 3", "bc": "False", "assertEqual": "две тысячи сто" },
+    "18": {"datetime_param": "1000, 1, 31, 13, 22, 3", "bc": "None", "assertEqual": "тысяча" },
+    "19": {"datetime_param": "2000, 1, 31, 13, 22, 3", "bc": "None", "assertEqual": "две тысячи" },
+    "20": {"datetime_param": "3120, 1, 31, 13, 22, 3", "bc": "True", "assertEqual": "три тысячи сто двадцать до нашей эры" },
+    "21": {"datetime_param": "3241, 1, 31, 13, 22, 3", "bc": "True", "assertEqual": "три тысячи двести сорок один до нашей эры" },
+    "22": {"datetime_param": "5200, 1, 31, 13, 22, 3", "bc": "False", "assertEqual": "пять тысяч двести" },
+    "23": {"datetime_param": "1100, 1, 31, 13, 22, 3", "bc": "False", "assertEqual": "тысяча сто" },
+    "24": {"datetime_param": "2100, 1, 31, 13, 22, 3", "bc": "False", "assertEqual": "две тысячи сто" }
+  },
+  "test_nice_date": {
+    "1": {"datetime_param": "2017, 1, 31, 0, 2, 3", "now": "None", "assertEqual": "во вторник, тридцать первого января, две тысячи семнадцать"},
+    "2": {"datetime_param": "2018, 2, 4, 0, 2, 3", "now": "2017, 1, 1, 0, 2, 3", "assertEqual": "в воскресенье, четвёртого февраля, две тысячи восемнадцать"},
+    "3": {"datetime_param": "2018, 2, 4, 0, 2, 3", "now": "2018, 1, 1, 0, 2, 3", "assertEqual": "в воскресенье, четвёртого февраля"},
+    "4": {"datetime_param": "2018, 2, 4, 0, 2, 3", "now": "2018, 2, 1, 0, 2, 3", "assertEqual": "в воскресенье, четвёртого"},
+    "5": {"datetime_param": "2018, 2, 4, 0, 2, 3", "now": "2018, 2, 3, 0, 2, 3", "assertEqual": "завтра"},
+    "6": {"datetime_param": "2018, 2, 4, 0, 2, 3", "now": "2018, 2, 4, 0, 2, 3", "assertEqual": "сегодня"},
+    "7": {"datetime_param": "2018, 2, 4, 0, 2, 3", "now": "2018, 2, 5, 0, 2, 3", "assertEqual": "вчера"},
+    "8": {"datetime_param": "2018, 2, 4, 0, 2, 3", "now": "2018, 2, 6, 0, 2, 3", "assertEqual": "в воскресенье, четвёртого февраля"},
+    "9": {"datetime_param": "2018, 2, 4, 0, 2, 3", "now": "2019, 2, 6, 0, 2, 3", "assertEqual": "в воскресенье, четвёртого февраля, две тысячи восемнадцать"}
+  },
+  "test_nice_date_time": {
+    "1": {"datetime_param": "2017, 1, 31, 13, 22, 3", "now": "None", "use_24hour": "False", "use_ampm": "True", "assertEqual": "во вторник, тридцать первого января, две тысячи семнадцать в час двадцать два дня"},
+    "2": {"datetime_param": "2017, 1, 31, 13, 22, 3", "now": "None", "use_24hour": "True", "use_ampm": "False", "assertEqual": "во вторник, тридцать первого января, две тысячи семнадцать в тринадцать двадцать два"}
+  }
+}
diff --git a/test/test_format_ru.py b/test/test_format_ru.py
new file mode 100644
index 00000000..8112a36b
--- /dev/null
+++ b/test/test_format_ru.py
@@ -0,0 +1,575 @@
+#
+# Copyright 2017 Mycroft AI Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import json
+import unittest
+import datetime
+import ast
+import sys
+from pathlib import Path
+
+from lingua_franca import get_default_lang, set_default_lang, \
+    load_language, unload_language
+from lingua_franca.format import date_time_format
+from lingua_franca.format import join_list
+from lingua_franca.format import nice_date
+from lingua_franca.format import nice_date_time
+from lingua_franca.format import nice_duration
+from lingua_franca.format import nice_number
+from lingua_franca.format import nice_time
+from lingua_franca.format import nice_year
+from lingua_franca.format import pronounce_number
+from lingua_franca.time import default_timezone
+
+
+def setUpModule():
+    load_language("ru-ru")
+    set_default_lang("ru")
+
+
+def tearDownModule():
+    unload_language("ru")
+
+
+NUMBERS_FIXTURE_RU = {
+    1.435634: '1.436',
+    2: '2',
+    5.0: '5',
+    0.027: '0.027',
+    0.5: 'половина',
+    1.333: '1 и 1 треть',
+    2.666: '2 и 2 трети',
+    0.25: 'четверть',
+    1.25: '1 и 1 четверть',
+    0.75: '3 четверти',
+    1.75: '1 и 3 четверти',
+    3.4: '3 и 2 пятые',
+    16.8333: '16 и 5 шестых',
+    12.5714: '12 и 4 седьмые',
+    9.625: '9 и 5 восьмых',
+    6.777: '6 и 7 девятых',
+    3.1: '3 и 1 десятая',
+    2.272: '2 и 3 одиннадцатые',
+    5.583: '5 и 7 двенадцатых',
+    8.384: '8 и 5 тринадцатых',
+    0.071: '1 четырнадцатая',
+    6.466: '6 и 7 пятнадцатых',
+    8.312: '8 и 5 шестнадцатых',
+    2.176: '2 и 3 семнадцатые',
+    200.722: '200 и 13 восемнадцатых',
+    7.421: '7 и 8 девятнадцатых',
+    0.05: '1 двадцатая'
+}
+
+
+class TestNiceNumberFormat(unittest.TestCase):
+
+    def test_convert_float_to_nice_number(self):
+        for number, number_str in NUMBERS_FIXTURE_RU.items():
+            self.assertEqual(nice_number(number, speech=True), number_str,
+                             'должен отформатировать {} как {}, а не {}'.format(
+                                 number, number_str, nice_number(number, speech=True)))
+
+    def test_specify_denominator(self):
+        self.assertEqual(nice_number(5.5, speech=True, denominators=[1, 2, 3]),
+                         '5 с половиной',
+                         'должен отформатировать 5.5 как 5 с половиной, а не {}'.format(
+                             nice_number(5.5, speech=True, denominators=[1, 2, 3])))
+        self.assertEqual(nice_number(2.333, speech=True, denominators=[1, 2]),
+                         '2.333',
+                         'должен отформатировать 2.333 как 2.333, а не {}'.format(
+                             nice_number(2.333, speech=True, denominators=[1, 2])))
+
+    def test_no_speech(self):
+        self.assertEqual(nice_number(6.777, speech=False),
+                         '6 7/9',
+                         'должен отформатировать 6.777 как 6 7/9, а не {}'.format(
+                             nice_number(6.777, speech=False)))
+        self.assertEqual(nice_number(6.0, speech=False),
+                         '6',
+                         'должен отформатировать 6.0 как 6, а не {}'.format(
+                             nice_number(6.0, speech=False)))
+
+
+class TestPronounceNumber(unittest.TestCase):
+
+    def test_convert_int(self):
+        self.assertEqual(pronounce_number(0), "ноль")
+        self.assertEqual(pronounce_number(1), "один")
+        self.assertEqual(pronounce_number(10), "десять")
+        self.assertEqual(pronounce_number(15), "пятнадцать")
+        self.assertEqual(pronounce_number(20), "двадцать")
+        self.assertEqual(pronounce_number(27), "двадцать семь")
+        self.assertEqual(pronounce_number(30), "тридцать")
+        self.assertEqual(pronounce_number(33), "тридцать три")
+
+    def test_convert_negative_int(self):
+        self.assertEqual(pronounce_number(-1), "минус один")
+        self.assertEqual(pronounce_number(-10), "минус десять")
+        self.assertEqual(pronounce_number(-15), "минус пятнадцать")
+        self.assertEqual(pronounce_number(-20), "минус двадцать")
+        self.assertEqual(pronounce_number(-27), "минус двадцать семь")
+        self.assertEqual(pronounce_number(-30), "минус тридцать")
+        self.assertEqual(pronounce_number(-33), "минус тридцать три")
+
+    def test_convert_decimals(self):
+        self.assertEqual(pronounce_number(0.05), "ноль точка ноль пять")
+        self.assertEqual(pronounce_number(-0.05), "минус ноль точка ноль пять")
+        self.assertEqual(pronounce_number(1.234),
+                         "один точка два три")
+        self.assertEqual(pronounce_number(21.234),
+                         "двадцать один точка два три")
+        self.assertEqual(pronounce_number(21.234, places=1),
+                         "двадцать один точка два")
+        self.assertEqual(pronounce_number(21.234, places=0),
+                         "двадцать один")
+        self.assertEqual(pronounce_number(21.234, places=3),
+                         "двадцать один точка два три четыре")
+        self.assertEqual(pronounce_number(21.234, places=4),
+                         "двадцать один точка два три четыре")
+        self.assertEqual(pronounce_number(21.234, places=5),
+                         "двадцать один точка два три четыре")
+        self.assertEqual(pronounce_number(-1.234),
+                         "минус один точка два три")
+        self.assertEqual(pronounce_number(-21.234),
+                         "минус двадцать один точка два три")
+        self.assertEqual(pronounce_number(-21.234, places=1),
+                         "минус двадцать один точка два")
+        self.assertEqual(pronounce_number(-21.234, places=0),
+                         "минус двадцать один")
+        self.assertEqual(pronounce_number(-21.234, places=3),
+                         "минус двадцать один точка два три четыре")
+        self.assertEqual(pronounce_number(-21.234, places=4),
+                         "минус двадцать один точка два три четыре")
+        self.assertEqual(pronounce_number(-21.234, places=5),
+                         "минус двадцать один точка два три четыре")
+
+    def test_convert_stos(self):
+        self.assertEqual(pronounce_number(100), "сто")
+        self.assertEqual(pronounce_number(666), "шестьсот шестьдесят шесть")
+        self.assertEqual(pronounce_number(1456), "тысяча четыреста пятьдесят шесть")
+        self.assertEqual(pronounce_number(103254654), "сто три миллиона "
+                                                      "двести пятьдесят "
+                                                      "четыре тысячи "
+                                                      "шестьсот "
+                                                      "пятьдесят четыре")
+        self.assertEqual(pronounce_number(1512457), "миллион пятьсот"
+                                                    " двенадцать тысяч "
+                                                    "четыреста пятьдесят "
+                                                    "семь")
+        self.assertEqual(pronounce_number(209996), "двести девять "
+                                                   "тысяч девятьсот "
+                                                   "девяносто шесть")
+
+    def test_convert_scientific_notation(self):
+        self.assertEqual(pronounce_number(0, scientific=True), "ноль")
+        self.assertEqual(pronounce_number(33, scientific=True),
+                         "три точка три на десять в степени один")
+        self.assertEqual(pronounce_number(299792458, scientific=True),
+                         "два точка девять девять на десять в степени восемь")
+        self.assertEqual(pronounce_number(299792458, places=6,
+                                          scientific=True),
+                         "два точка девять девять семь девять два пять "
+                         "на десять в степени восемь")
+        self.assertEqual(pronounce_number(1.672e-27, places=3,
+                                          scientific=True),
+                         "один точка шесть семь два на десять в степени "
+                         "минус двадцать семь")
+
+    def test_auto_scientific_notation(self):
+        self.assertEqual(
+            pronounce_number(1.1e-150), "один точка один на десять в степени "
+                                        "минус сто пятьдесят")
+
+    def test_large_numbers(self):
+        self.maxDiff = None
+        self.assertEqual(
+            pronounce_number(299792458, short_scale=True),
+            "двести девяносто девять миллионов семьсот "
+            "девяносто две тысячи четыреста пятьдесят восемь")
+        self.assertEqual(
+            pronounce_number(299792458, short_scale=False),
+            "двести девяносто девять миллионов семьсот "
+            "девяносто две тысячи четыреста пятьдесят восемь")
+        self.assertEqual(
+            pronounce_number(100034000000299792458, short_scale=True),
+            "сто квинтиллионов тридцать четыре квадриллиона "
+            "двести девяносто девять миллионов семьсот "
+            "девяносто две тысячи четыреста пятьдесят восемь")
+        self.assertEqual(
+            pronounce_number(100034000000299792458, short_scale=False),
+            "сто биллионов тридцать четыре тысячи миллиардов "
+            "двести девяносто девять миллионов семьсот "
+            "девяносто две тысячи четыреста пятьдесят восемь")
+        self.assertEqual(
+            pronounce_number(1e10, short_scale=True),
+            "десять миллиардов")
+        self.assertEqual(
+            pronounce_number(1e12, short_scale=True),
+            "триллион")
+        # TODO maybe beautify this
+        self.assertEqual(
+            pronounce_number(1000001, short_scale=True),
+            "миллион один")
+        self.assertEqual(pronounce_number(95505896639631893, short_scale=True),
+                         "девяносто пять квадриллионов "
+                         "пятьсот пять триллионов "
+                         "восемьсот девяносто шесть миллиардов "
+                         "шестьсот тридцать девять миллионов "
+                         "шестьсот тридцать одна тысяча "
+                         "восемьсот девяносто три")
+        self.assertEqual(pronounce_number(95505896639631893,
+                                          short_scale=False),
+                         "девяносто пять тысяч пятьсот пять миллиардов "
+                         "восемьсот девяносто шесть тысяч "
+                         "шестьсот тридцать девять миллионов "
+                         "шестьсот тридцать одна тысяча "
+                         "восемьсот девяносто три")
+        self.assertEqual(pronounce_number(10e80, places=1),
+                         "секснвигинтиллион")
+        # TODO floating point rounding issues might happen
+        self.assertEqual(pronounce_number(1.9874522571e80, places=9),
+                         "сто девяносто восемь квинвигинтиллионов "
+                         "семьсот сорок пять кватторвигинтиллионов "
+                         "двести двадцать пять тревигинтиллионов "
+                         "семьсот девять дуовигинтиллионов "
+                         "девятьсот девяносто девять унвигинтиллионов "
+                         "девятьсот восемьдесят девять вигинтиллионов "
+                         "семьсот тридцать новемдециллионов "
+                         "девятьсот девятнадцать октодециллионов "
+                         "девятьсот девяносто девять септендециллионов "
+                         "девятьсот пятьдесят пять сексдециллионов "
+                         "четыреста девяносто восемь квиндециллионов "
+                         "двести четырнадцать кваттордециллионов "
+                         "восемьсот сорок пять тредециллионов "
+                         "четыреста двадцать девять дуодециллионов "
+                         "четыреста сорок четыре ундециллиона "
+                         "триста тридцать шесть дециллионов "
+                         "семьсот двадцать четыре нониллиона "
+                         "пятьсот шестьдесят девять октиллионов "
+                         "триста семьдесят пять септиллионов "
+                         "двести тридцать девять секстиллионов "
+                         "шестьсот семьдесят квинтиллионов "
+                         "пятьсот семьдесят четыре квадриллиона "
+                         "семьсот тридцать девять триллионов "
+                         "семьсот сорок восемь миллиардов "
+                         "четыреста семьдесят миллионов "
+                         "девятьсот пятнадцать тысяч "
+                         "семьдесят два")
+
+        # infinity
+        self.assertEqual(
+            pronounce_number(sys.float_info.max * 2), "бесконечность")
+        self.assertEqual(
+            pronounce_number(float("inf")),
+            "бесконечность")
+        self.assertEqual(
+            pronounce_number(float("-inf")),
+            "минус бесконечность")
+
+    def test_ordinals(self):
+        self.assertEqual(pronounce_number(1, ordinals=True), "первый")
+        self.assertEqual(pronounce_number(10, ordinals=True), "десятый")
+        self.assertEqual(pronounce_number(15, ordinals=True), "пятнадцатый")
+        self.assertEqual(pronounce_number(20, ordinals=True), "двадцатый")
+        self.assertEqual(pronounce_number(27, ordinals=True), "двадцать седьмой")
+        self.assertEqual(pronounce_number(30, ordinals=True), "тридцатый")
+        self.assertEqual(pronounce_number(33, ordinals=True), "тридцать третий")
+        self.assertEqual(pronounce_number(100, ordinals=True), "сотый")
+        self.assertEqual(pronounce_number(1000, ordinals=True), "тысячный")
+        self.assertEqual(pronounce_number(10000, ordinals=True),
+                         "десятитысячный")
+        self.assertEqual(pronounce_number(18691, ordinals=True),
+                         "восемнадцать тысяч шестьсот девяносто первый")
+        self.assertEqual(pronounce_number(1567, ordinals=True),
+                         "тысяча пятьсот шестьдесят седьмой")
+        self.assertEqual(pronounce_number(1.672e-27, places=3,
+                                          scientific=True, ordinals=True),
+                         "один точка шесть семь два на десять в минус "
+                         "двадцать седьмой степени")
+        self.assertEqual(pronounce_number(1e6, ordinals=True),
+                         "миллионный")
+        self.assertEqual(pronounce_number(2e6, ordinals=True),
+                         "двухмиллионный")
+        self.assertEqual(pronounce_number(2e6, ordinals=True, short_scale=False),
+                         "двухмиллионный")
+        self.assertEqual(pronounce_number(3e6, ordinals=True),
+                         "трёхмиллионный")
+        self.assertEqual(pronounce_number(4e6, ordinals=True),
+                         "четырёхмиллионный")
+        self.assertEqual(pronounce_number(18e6, ordinals=True),
+                         "восемнадцатимиллионный")
+        self.assertEqual(pronounce_number(18e12, ordinals=True,
+                                          short_scale=False),
+                         "восемнадцатибиллионный")
+        self.assertEqual(pronounce_number(18e12, ordinals=True),
+                         "восемнадцатитриллионный")
+        self.assertEqual(pronounce_number(18e18, ordinals=True,
+                                          short_scale=False), "восемнадцатитриллионный")
+
+
+class TestNiceDateFormat(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        # Read date_time_test.json files for test data
+        cls.test_config = {}
+        p = Path(date_time_format.config_path)
+        for sub_dir in [x for x in p.iterdir() if x.is_dir()]:
+            if (sub_dir / 'date_time_test.json').exists():
+                print("Loading test for " +
+                      str(sub_dir / 'date_time_test.json'))
+                with (sub_dir / 'date_time_test.json').open() as f:
+                    cls.test_config[sub_dir.parts[-1]] = json.loads(f.read())
+
+    def test_convert_times(self):
+        dt = datetime.datetime(2017, 1, 31,
+                               13, 22, 3, tzinfo=default_timezone())
+
+        # Verify defaults haven't changed
+        self.assertEqual(nice_time(dt),
+                         nice_time(dt, speech=True, use_24hour=True, use_ampm=False))
+
+        self.assertEqual(nice_time(dt, use_24hour=False),
+                         "час двадцать два")
+        self.assertEqual(nice_time(dt, use_24hour=False, use_ampm=True),
+                         "час двадцать два дня")
+        self.assertEqual(nice_time(dt, speech=False, use_24hour=False),
+                         "1:22")
+        self.assertEqual(nice_time(dt, speech=False, use_24hour=False, use_ampm=True),
+                         "1:22 дня")
+        self.assertEqual(nice_time(dt, speech=False, use_24hour=True),
+                         "13:22")
+        self.assertEqual(nice_time(dt, speech=False, use_24hour=True,
+                                   use_ampm=True),
+                         "13:22")
+        self.assertEqual(nice_time(dt, use_24hour=True, use_ampm=True),
+                         "тринадцать двадцать два")
+        self.assertEqual(nice_time(dt, use_24hour=True, use_ampm=False),
+                         "тринадцать двадцать два")
+
+        dt = datetime.datetime(2017, 1, 31,
+                               13, 0, 3, tzinfo=default_timezone())
+        self.assertEqual(nice_time(dt, use_24hour=False),
+                         "час")
+        self.assertEqual(nice_time(dt, use_24hour=False, use_ampm=True),
+                         "час дня")
+        self.assertEqual(nice_time(dt, use_24hour=False, speech=False),
+                         "1:00")
+        self.assertEqual(nice_time(dt, speech=False, use_24hour=False, use_ampm=True),
+                         "1:00 дня")
+        self.assertEqual(nice_time(dt, speech=False, use_24hour=True),
+                         "13:00")
+        self.assertEqual(nice_time(dt, speech=False, use_24hour=True,
+                                   use_ampm=True),
+                         "13:00")
+        self.assertEqual(nice_time(dt, use_24hour=True, use_ampm=True),
+                         "тринадцать ровно")
+        self.assertEqual(nice_time(dt, use_24hour=True, use_ampm=False),
+                         "тринадцать ровно")
+
+        dt = datetime.datetime(2017, 1, 31,
+                               13, 2, 3, tzinfo=default_timezone())
+        self.assertEqual(nice_time(dt, use_24hour=False),
+                         "час ноль два")
+        self.assertEqual(nice_time(dt, use_24hour=False, use_ampm=True),
+                         "час ноль два дня")
+        self.assertEqual(nice_time(dt, use_24hour=False, speech=False),
+                         "1:02")
+        self.assertEqual(nice_time(dt, use_24hour=False, speech=False, use_ampm=True),
+                         "1:02 дня")
+        self.assertEqual(nice_time(dt, speech=False, use_24hour=True),
+                         "13:02")
+        self.assertEqual(nice_time(dt, speech=False, use_24hour=True,
+                                   use_ampm=True),
+                         "13:02")
+        self.assertEqual(nice_time(dt, use_24hour=True, use_ampm=True),
+                         "тринадцать ноль два")
+        self.assertEqual(nice_time(dt, use_24hour=True, use_ampm=False),
+                         "тринадцать ноль два")
+
+        dt = datetime.datetime(2017, 1, 31,
+                               0, 2, 3, tzinfo=default_timezone())
+        self.assertEqual(nice_time(dt, use_24hour=False),
+                         "двенадцать ноль два")
+        self.assertEqual(nice_time(dt, use_24hour=False, use_ampm=True),
+                         "двенадцать ноль два ночи")
+        self.assertEqual(nice_time(dt, speech=False, use_24hour=False),
+                         "12:02")
+        self.assertEqual(nice_time(dt, speech=False, use_24hour=False, use_ampm=True),
+                         "12:02 ночи")
+        self.assertEqual(nice_time(dt, speech=False, use_24hour=True),
+                         "00:02")
+        self.assertEqual(nice_time(dt, speech=False, use_24hour=True,
+                                   use_ampm=True),
+                         "00:02")
+        self.assertEqual(nice_time(dt, use_24hour=True, use_ampm=True),
+                         "ноль ноль ноль два")
+        self.assertEqual(nice_time(dt, use_24hour=True, use_ampm=False),
+                         "ноль ноль ноль два")
+
+        dt = datetime.datetime(2018, 2, 8,
+                               1, 2, 33, tzinfo=default_timezone())
+        self.assertEqual(nice_time(dt, use_24hour=False),
+                         "час ноль два")
+        self.assertEqual(nice_time(dt, use_24hour=False, use_ampm=True),
+                         "час ноль два ночи")
+        self.assertEqual(nice_time(dt, speech=False, use_24hour=False),
+                         "1:02")
+        self.assertEqual(nice_time(dt, speech=False, use_24hour=False, use_ampm=True),
+                         "1:02 ночи")
+        self.assertEqual(nice_time(dt, speech=False, use_24hour=True),
+                         "01:02")
+        self.assertEqual(nice_time(dt, speech=False, use_24hour=True,
+                                   use_ampm=True),
+                         "01:02")
+        self.assertEqual(nice_time(dt, use_24hour=True, use_ampm=True),
+                         "ноль один ноль два")
+        self.assertEqual(nice_time(dt, use_24hour=True, use_ampm=False),
+                         "ноль один ноль два")
+
+        dt = datetime.datetime(2017, 1, 31,
+                               12, 15, 9, tzinfo=default_timezone())
+        self.assertEqual(nice_time(dt, use_24hour=False),
+                         "двенадцать с четвертью")
+        self.assertEqual(nice_time(dt, use_24hour=False, use_ampm=True),
+                         "двенадцать с четвертью дня")
+
+        dt = datetime.datetime(2017, 1, 31,
+                               5, 30, 00, tzinfo=default_timezone())
+        self.assertEqual(nice_time(dt, use_24hour=False, use_ampm=True),
+                         "пять с половиной утра")
+
+        dt = datetime.datetime(2017, 1, 31,
+                               1, 45, 00, tzinfo=default_timezone())
+        self.assertEqual(nice_time(dt, use_24hour=False),
+                         "без четверти два")
+
+    def test_nice_date(self):
+        lang = "ru-ru"
+        i = 1
+        while (self.test_config[lang].get('test_nice_date') and
+               self.test_config[lang]['test_nice_date'].get(str(i).encode('utf8'))):
+            p = self.test_config[lang]['test_nice_date'][str(i)]
+            dp = ast.literal_eval(p['datetime_param'])
+            np = ast.literal_eval(p['now'])
+            dt = datetime.datetime(
+                dp[0], dp[1], dp[2], dp[3], dp[4], dp[5],
+                tzinfo=default_timezone())
+            now = None if not np else datetime.datetime(
+                np[0], np[1], np[2], np[3], np[4], np[5],
+                tzinfo=default_timezone())
+            print('Testing for ' + lang + ' that ' + str(dt) +
+                  ' is date ' + p['assertEqual'])
+            self.assertEqual(p['assertEqual'],
+                             nice_date(dt, lang=lang, now=now))
+            i = i + 1
+
+        # test all days in a year for all languages,
+        # that some output is produced
+        # for lang in self.test_config:
+        for dt in (datetime.datetime(2017, 12, 30, 0, 2, 3,
+                                     tzinfo=default_timezone()) +
+                   datetime.timedelta(n) for n in range(368)):
+            self.assertTrue(len(nice_date(dt, lang=lang)) > 0)
+
+    def test_nice_date_time(self):
+        lang = "ru-ru"
+        i = 1
+        while (self.test_config[lang].get('test_nice_date_time') and
+               self.test_config[lang]['test_nice_date_time'].get(str(i).encode('utf8'))):
+            p = self.test_config[lang]['test_nice_date_time'][str(i)]
+            dp = ast.literal_eval(p['datetime_param'])
+            np = ast.literal_eval(p['now'])
+            dt = datetime.datetime(
+                dp[0], dp[1], dp[2], dp[3], dp[4], dp[5],
+                tzinfo=default_timezone())
+            now = None if not np else datetime.datetime(
+                np[0], np[1], np[2], np[3], np[4], np[5])
+            print('Testing for ' + lang + ' that ' + str(dt) +
+                  ' is date time ' + p['assertEqual'])
+            self.assertEqual(
+                p['assertEqual'],
+                nice_date_time(
+                    dt, lang=lang, now=now,
+                    use_24hour=ast.literal_eval(p['use_24hour']),
+                    use_ampm=ast.literal_eval(p['use_ampm'])))
+            i = i + 1
+
+    def test_nice_year(self):
+        lang = "ru-ru"
+        i = 1
+        while (self.test_config[lang].get('test_nice_year') and
+               self.test_config[lang]['test_nice_year'].get(str(i).encode('utf8'))):
+            p = self.test_config[lang]['test_nice_year'][str(i)]
+            dp = ast.literal_eval(p['datetime_param'])
+            dt = datetime.datetime(
+                dp[0], dp[1], dp[2], dp[3], dp[4], dp[5],
+                tzinfo=default_timezone())
+            print('Testing for ' + lang + ' that ' + str(dt) +
+                  ' is year ' + p['assertEqual'])
+            self.assertEqual(p['assertEqual'], nice_year(
+                dt, lang=lang, bc=ast.literal_eval(p['bc'])))
+            i = i + 1
+
+        # Test all years from 0 to 9999 for all languages,
+        # that some output is produced
+        print("Test all years in " + lang)
+        for i in range(1, 9999):
+            dt = datetime.datetime(i, 1, 31, 13, 2, 3,
+                                   tzinfo=default_timezone())
+            self.assertTrue(len(nice_year(dt, lang=lang)) > 0)
+            # Looking through the date sequence can be helpful
+
+    def test_nice_duration(self):
+
+        self.assertEqual(nice_duration(1), "одна секунда")
+        self.assertEqual(nice_duration(3), "три секунды")
+        self.assertEqual(nice_duration(1, speech=False), "0:01")
+        self.assertEqual(nice_duration(61), "одна минута одна секунда")
+        self.assertEqual(nice_duration(61, speech=False), "1:01")
+        self.assertEqual(nice_duration(5000),
+                         "один час двадцать три минуты двадцать секунд")
+        self.assertEqual(nice_duration(5000, speech=False), "1:23:20")
+        self.assertEqual(nice_duration(50000),
+                         "тринадцать часов пятьдесят три минуты двадцать секунд")
+        self.assertEqual(nice_duration(50000, speech=False), "13:53:20")
+        self.assertEqual(nice_duration(500000),
+                         "пять дней восемнадцать часов пятьдесят три минуты двадцать секунд")  # nopep8
+        self.assertEqual(nice_duration(500000, speech=False), "5d 18:53:20")
+        self.assertEqual(nice_duration(datetime.timedelta(seconds=500000),
+                                       speech=False),
+                         "5d 18:53:20")
+
+    def test_join(self):
+        self.assertEqual(join_list(None, "и"), "")
+        self.assertEqual(join_list([], "и"), "")
+
+        self.assertEqual(join_list(["a"], "и"), "a")
+        self.assertEqual(join_list(["a", "b"], "и"), "a и b")
+        self.assertEqual(join_list(["a", "b"], "или"), "a или b")
+
+        self.assertEqual(join_list(["a", "b", "c"], "и"), "a, b и c")
+        self.assertEqual(join_list(["a", "b", "c"], "или"), "a, b или c")
+        self.assertEqual(
+            join_list(["a", "b", "c"], "или", ";"), "a; b или c")
+        self.assertEqual(
+            join_list(["a", "b", "c", "d"], "или"), "a, b, c или d")
+
+        self.assertEqual(join_list([1, "b", 3, "d"], "или"), "1, b, 3 или d")
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/test/test_parse_ru.py b/test/test_parse_ru.py
new file mode 100644
index 00000000..7792a436
--- /dev/null
+++ b/test/test_parse_ru.py
@@ -0,0 +1,751 @@
+#
+# Copyright 2017 Mycroft AI Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+import unittest
+from datetime import datetime, timedelta
+
+from lingua_franca import set_default_lang, \
+    load_language, unload_language
+from lingua_franca.parse import extract_datetime
+from lingua_franca.parse import extract_duration
+from lingua_franca.parse import extract_number, extract_numbers
+from lingua_franca.parse import fuzzy_match
+from lingua_franca.parse import match_one
+from lingua_franca.parse import normalize
+from lingua_franca.time import default_timezone
+
+
+def setUpModule():
+    load_language("ru-ru")
+    set_default_lang("ru")
+
+
+def tearDownModule():
+    unload_language("ru")
+
+
+class TestFuzzyMatch(unittest.TestCase):
+    def test_matches(self):
+        self.assertTrue(fuzzy_match("ты и мы", "ты и мы") >= 1.0)
+        self.assertTrue(fuzzy_match("ты и мы", "ты") < 0.5)
+        self.assertTrue(fuzzy_match("Ты", "ты") >= 0.5)
+        self.assertTrue(fuzzy_match("ты и мы", "ты") ==
+                        fuzzy_match("ты", "ты и мы"))
+        self.assertTrue(fuzzy_match("ты и мы", "он или они") < 0.36)
+
+    def test_match_one(self):
+        # test list of choices
+        choices = ['фрэнк', 'кейт', 'гарри', 'генри']
+        self.assertEqual(match_one('фрэнк', choices)[0], 'фрэнк')
+        self.assertEqual(match_one('фрэн', choices)[0], 'фрэнк')
+        self.assertEqual(match_one('енри', choices)[0], 'генри')
+        self.assertEqual(match_one('кэтт', choices)[0], 'кейт')
+        # test dictionary of choices
+        choices = {'фрэнк': 1, 'кейт': 2, 'гарри': 3, 'генри': 4}
+        self.assertEqual(match_one('фрэнк', choices)[0], 1)
+        self.assertEqual(match_one('енри', choices)[0], 4)
+
+
+class TestNormalize(unittest.TestCase):
+
+    def test_extract_number(self):
+        self.assertEqual(extract_number("это первый тест",
+                                        ordinals=True), 1)
+        self.assertEqual(extract_number("это 2 тест"), 2)
+        self.assertEqual(extract_number("это второй тест",
+                                        ordinals=True), 2)
+        # self.assertEqual(extract_number("этот один третий тест"), 1.0 / 3.0)
+        self.assertEqual(extract_number("этот один третий тест",
+                                        ordinals=True), 3.0)
+        self.assertEqual(extract_number("это четвёртый", ordinals=True), 4.0)
+        self.assertEqual(extract_number(
+            "это тридцать шестой", ordinals=True), 36.0)
+        self.assertEqual(extract_number("это тест на число 4"), 4)
+        self.assertEqual(extract_number("одна треть чашки"), 1.0 / 3.0)
+        self.assertEqual(extract_number("три чашки"), 3)
+        self.assertEqual(extract_number("1/3 чашки"), 1.0 / 3.0)
+        self.assertEqual(extract_number("четверть чашки"), 0.25)
+        self.assertEqual(extract_number("одна четвёртая чашки"), 0.25)
+        self.assertEqual(extract_number("1/4 чашки"), 0.25)
+        self.assertEqual(extract_number("2/3 чашки"), 2.0 / 3.0)
+        self.assertEqual(extract_number("3/4 чашки"), 3.0 / 4.0)
+        self.assertEqual(extract_number("1 и 3/4 чашки"), 1.75)
+        self.assertEqual(extract_number("1 чашка с половиной"), 1.5)
+        self.assertEqual(extract_number("один чашка с половиной"), 1.5)
+        self.assertEqual(extract_number("одна и половина чашки"), 1.5)
+        self.assertEqual(extract_number("одна с половиной чашка"), 1.5)
+        self.assertEqual(extract_number("одна и одна половина чашки"), 1.5)
+        # self.assertEqual(extract_number("три четверти чашки"), 3.0 / 4.0)
+        # self.assertEqual(extract_number("три четвёртые чашки"), 3.0 / 4.0)
+        self.assertEqual(extract_number("двадцать два"), 22)
+        self.assertEqual(extract_number(
+            "Двадцать два с заглавной буквой в начале"), 22)
+        self.assertEqual(extract_number(
+            "Двадцать Два с двумя заглавными буквами"), 22)
+        self.assertEqual(extract_number(
+            "двадцать Два с другой заглавной буквой"), 22)
+        # self.assertEqual(extract_number("Двадцать два и Три Пятых"), 22.6)
+        self.assertEqual(extract_number("двести"), 200)
+        self.assertEqual(extract_number("девять тысяч"), 9000)
+        self.assertEqual(extract_number("шестьсот шестьдесят шесть"), 666)
+        self.assertEqual(extract_number("два миллиона"), 2000000)
+        self.assertEqual(extract_number("два миллиона пятьсот тысяч "
+                                        "тонн чугуна"), 2500000)
+        self.assertEqual(extract_number("шесть триллионов"), 6000000000000.0)
+        self.assertEqual(extract_number("шесть триллионов", short_scale=False),
+                         6e+18)
+        self.assertEqual(extract_number("один точка пять"), 1.5)
+        self.assertEqual(extract_number("три точка четырнадцать"), 3.14)
+        self.assertEqual(extract_number("ноль точка два"), 0.2)
+        self.assertEqual(extract_number("миллиард лет"),
+                         1000000000.0)
+        self.assertEqual(extract_number("биллион лет",
+                                        short_scale=False),
+                         1000000000000.0)
+        self.assertEqual(extract_number("сто тысяч"), 100000)
+        self.assertEqual(extract_number("минус 2"), -2)
+        self.assertEqual(extract_number("минус семьдесят"), -70)
+        self.assertEqual(extract_number("тысяча миллионов"), 1000000000)
+        self.assertEqual(extract_number("миллиард", short_scale=False),
+                         1000000000)
+        # self.assertEqual(extract_number("шестая треть"),
+        #                  1 / 6 / 3)
+        # self.assertEqual(extract_number("шестая треть", ordinals=True),
+        #                  6)
+        self.assertEqual(extract_number("тридцать секунд"), 30)
+        self.assertEqual(extract_number("тридцать два", ordinals=True), 32)
+        self.assertEqual(extract_number("вот это миллиардный тест",
+                                        ordinals=True), 1e09)
+        self.assertEqual(extract_number("вот это одна миллиардная теста"), 1e-9)
+        self.assertEqual(extract_number("вот это биллионный тест",
+                                        ordinals=True,
+                                        short_scale=False), 1e12)
+        # self.assertEqual(extract_number("вот это одна биллионная теста",
+        #                                 short_scale=False), 1e-12)
+
+        # Verify non-power multiples of ten no longer discard
+        # adjacent multipliers
+        self.assertEqual(extract_number("двадцать тысяч"), 20000)
+        self.assertEqual(extract_number("пятьдесят миллионов"), 50000000)
+
+        # Verify smaller powers of ten no longer cause miscalculation of larger
+        # powers of ten (see MycroftAI#86)
+        self.assertEqual(extract_number("двадцать миллиардов триста миллионов "
+                                        "девятьсот пятьдесят тысяч "
+                                        "шестьсот семьдесят пять точка восемь"),
+                         20300950675.8)
+        self.assertEqual(extract_number("девятьсот девяносто девять миллионов "
+                                        "девятьсот девяносто девять тысяч "
+                                        "девятьсот девяносто девять точка девять"),
+                         999999999.9)
+
+        # TODO why does "trillion" result in xxxx.0?
+        self.assertEqual(extract_number("восемьсот триллионов двести \
+                                        пятьдесят семь"), 800000000000257.0)
+
+        # TODO handle this case
+        # self.assertEqual(
+        #    extract_number("6 dot six six six"),
+        #    6.666)
+        self.assertTrue(extract_number("Теннисист скорый") is False)
+        self.assertTrue(extract_number("хрупкий") is False)
+
+        self.assertTrue(extract_number("хрупкий ноль") is not False)
+        self.assertEqual(extract_number("хрупкий ноль"), 0)
+
+        self.assertTrue(extract_number("грубый 0") is not False)
+        self.assertEqual(extract_number("грубый 0"), 0)
+
+        self.assertEqual(extract_number("пара пива"), 2)
+        # self.assertEqual(extract_number("пара сотен пив"), 200)
+        self.assertEqual(extract_number("пара тысяч пив"), 2000)
+
+        self.assertEqual(extract_number(
+            "вот это 7 тест", ordinals=True), 7)
+        self.assertEqual(extract_number(
+            "вот это 7 тест", ordinals=False), 7)
+        self.assertTrue(extract_number("вот это n. тест") is False)
+        self.assertEqual(extract_number("вот это 1. тест"), 1)
+        self.assertEqual(extract_number("вот это 2. тест"), 2)
+        self.assertEqual(extract_number("вот это 3. тест"), 3)
+        self.assertEqual(extract_number("вот это 31. тест"), 31)
+        self.assertEqual(extract_number("вот это 32. тест"), 32)
+        self.assertEqual(extract_number("вот это 33. тест"), 33)
+        self.assertEqual(extract_number("вот это 34. тест"), 34)
+        self.assertEqual(extract_number("в общем 100%"), 100)
+
+    def test_extract_duration_ru(self):
+        self.assertEqual(extract_duration("10 секунд"),
+                         (timedelta(seconds=10.0), ""))
+        self.assertEqual(extract_duration("5 минут"),
+                         (timedelta(minutes=5), ""))
+        self.assertEqual(extract_duration("2 часа"),
+                         (timedelta(hours=2), ""))
+        self.assertEqual(extract_duration("3 дня"),
+                         (timedelta(days=3), ""))
+        self.assertEqual(extract_duration("25 недель"),
+                         (timedelta(weeks=25), ""))
+        self.assertEqual(extract_duration("семь часов"),
+                         (timedelta(hours=7), ""))
+        self.assertEqual(extract_duration("7.5 секунд"),
+                         (timedelta(seconds=7.5), ""))
+        self.assertEqual(extract_duration("восемь с половиной дней "
+                                          "тридцать девять секунд"),
+                         (timedelta(days=8.5, seconds=39), ""))
+        self.assertEqual(extract_duration("Установи таймер на 30 минут"),
+                         (timedelta(minutes=30), "установи таймер на"))
+        self.assertEqual(extract_duration("Четыре с половиной минуты до"
+                                          " заката"),
+                         (timedelta(minutes=4.5), "до заката"))
+        self.assertEqual(extract_duration("девятнадцать минут через час"),
+                         (timedelta(minutes=19), "через час"))
+        # self.assertEqual(extract_duration("разбуди меня через три недели, "
+        #                                   "четыреста девяносто семь дней "
+        #                                   "и триста 91.6 секунд"),
+        #                  (timedelta(weeks=3, days=497, seconds=391.6),
+        #                   "разбуди меня через , a"))
+        self.assertEqual(extract_duration("фильм один час пятьдесят семь"
+                                          " и пол минуты длиной"),
+                         (timedelta(hours=1, minutes=57.5),
+                          "фильм   длиной"))
+        self.assertEqual(extract_duration("10-секунд"),
+                         (timedelta(seconds=10.0), ""))
+        self.assertEqual(extract_duration("5-минут"),
+                         (timedelta(minutes=5), ""))
+
+    def test_extractdatetime_ru(self):
+        def extractWithFormat(text):
+            # Tue June 27, 2017 @ 1:04pm
+            date = datetime(2017, 6, 27, 13, 4, tzinfo=default_timezone())
+            [extractedDate, leftover] = extract_datetime(text, date)
+            extractedDate = extractedDate.strftime("%Y-%m-%d %H:%M:%S")
+            return [extractedDate, leftover]
+
+        def testExtract(text, expected_date, expected_leftover):
+            res = extractWithFormat(normalize(text))
+            self.assertEqual(res[0], expected_date, "for=" + text)
+            self.assertEqual(res[1], expected_leftover, "for=" + text)
+
+        testExtract("теперь пора",
+                    "2017-06-27 13:04:00", "пора")
+        self.u = "секунду"
+        testExtract("через %s" % self.u,
+                    "2017-06-27 13:04:01", "")
+        testExtract("через минуту",
+                    "2017-06-27 13:05:00", "")
+        testExtract("через две минуты",
+                    "2017-06-27 13:06:00", "")
+        # testExtract("через пару минут",
+        #            "2017-06-27 13:06:00", "")
+        testExtract("через два часа",
+                    "2017-06-27 15:04:00", "")
+        # testExtract("через пару часов",
+        #            "2017-06-27 15:04:00", "")
+        testExtract("через две недели",
+                   "2017-07-11 00:00:00", "")
+        # testExtract("через пару недель",
+        #            "2017-07-11 00:00:00", "")
+        testExtract("через два месяца",
+                    "2017-08-27 00:00:00", "")
+        testExtract("через два года",
+                    "2019-06-27 00:00:00", "")
+        # testExtract("через пару месяцев",
+        #            "2017-08-27 00:00:00", "")
+        # testExtract("через пару лет",
+        #            "2019-06-27 00:00:00", "")
+        testExtract("через десятилетие",
+                    "2027-06-27 00:00:00", "")
+        # testExtract("через пару десятилетий",
+        #            "2037-06-27 00:00:00", "")
+        testExtract("следующее десятилетие",
+                    "2027-06-27 00:00:00", "")
+        testExtract("через столетие",
+                    "2117-06-27 00:00:00", "")
+        testExtract("через тысячелетие",
+                    "3017-06-27 00:00:00", "")
+        # testExtract("через два десятилетия",
+        #             "2037-06-27 00:00:00", "")
+        # testExtract("через 5 десятилетий",
+        #             "2067-06-27 00:00:00", "")
+        # testExtract("через два века",
+        #             "2217-06-27 00:00:00", "")
+        # testExtract("через пару веков",
+        #            "2217-06-27 00:00:00", "")
+        # testExtract("через два тысячелетия",
+        #             "4017-06-27 00:00:00", "")
+        # testExtract("через две тысячи лет",
+        #             "4017-06-27 00:00:00", "")
+        # testExtract("через пару тысячелетий",
+        #            "4017-06-27 00:00:00", "")
+        # testExtract("через пару тысяч лет",
+        #            "4017-06-27 00:00:00", "")
+        testExtract("через год",
+                    "2018-06-27 00:00:00", "")
+        testExtract("хочу мороженое через час",
+                    "2017-06-27 14:04:00", "хочу мороженое")
+        testExtract("через 1 секунду",
+                    "2017-06-27 13:04:01", "")
+        testExtract("через 2 секунды",
+                    "2017-06-27 13:04:02", "")
+        testExtract("Установи таймер на 1 минуту",
+                    "2017-06-27 13:05:00", "установи таймер")
+        testExtract("Установи таймер на пол часа",
+                    "2017-06-27 13:34:00", "установи таймер")
+        # testExtract("Установи таймер на 5 дней с сегодня",
+        #             "2017-07-02 00:00:00", "установи таймер")
+        testExtract("послезавтра",
+                    "2017-06-29 00:00:00", "")
+        testExtract("после завтра",
+                    "2017-06-29 00:00:00", "")
+        testExtract("Какая погода послезавтра?",
+                    "2017-06-29 00:00:00", "какая погода")
+        testExtract("Напомни мне в 10:45 pm",
+                    "2017-06-27 22:45:00", "напомни мне")
+        testExtract("Напомни мне в 10:45 вечера",
+                    "2017-06-27 22:45:00", "напомни мне")
+        testExtract("какая погода в пятницу утром",
+                    "2017-06-30 08:00:00", "какая погода")
+        testExtract("какая завтра погода",
+                    "2017-06-28 00:00:00", "какая погода")
+        testExtract("какая погода сегодня днём",
+                    "2017-06-27 15:00:00", "какая погода")
+        testExtract("какая погода сегодня вечером",
+                    "2017-06-27 19:00:00", "какая погода")
+        testExtract("какая была погода сегодня утром",
+                    "2017-06-27 08:00:00", "какая была погода")
+        testExtract("напомни мне позвонить маме через 8 недель и 2 дня",
+                    "2017-08-24 00:00:00", "напомни мне позвонить маме")
+        testExtract("напомни мне позвонить маме в августе 3",
+                    "2017-08-03 00:00:00", "напомни мне позвонить маме")
+        testExtract("напомни мне завтра позвонить маме в 7am",
+                    "2017-06-28 07:00:00", "напомни мне позвонить маме")
+        testExtract("напомни мне завтра позвонить маме в 7утра",
+                    "2017-06-28 07:00:00", "напомни мне позвонить маме")
+        testExtract("напомни мне завтра позвонить маме в 10pm",
+                    "2017-06-28 22:00:00", "напомни мне позвонить маме")
+        testExtract("напомни мне завтра позвонить маме в 7 вечера",
+                    "2017-06-28 19:00:00", "напомни мне позвонить маме")
+        testExtract("напомни мне завтра позвонить маме в 10 вечера",
+                    "2017-06-28 22:00:00", "напомни мне позвонить маме")
+        testExtract("напомни мне завтра позвонить маме в 7 часов вечера",
+                    "2017-06-28 19:00:00", "напомни мне позвонить маме")
+        testExtract("напомни мне завтра позвонить маме в 10 часов вечера",
+                    "2017-06-28 22:00:00", "напомни мне позвонить маме")
+        testExtract("напомни мне позвонить маме в 7am",
+                    "2017-06-28 07:00:00", "напомни мне позвонить маме")
+        testExtract("напомни мне позвонить маме в 7утра",
+                    "2017-06-28 07:00:00", "напомни мне позвонить маме")
+        testExtract("напомни мне позвонить маме через час",
+                    "2017-06-27 14:04:00", "напомни мне позвонить маме")
+        testExtract("напомни мне позвонить маме в 1730",
+                    "2017-06-27 17:30:00", "напомни мне позвонить маме")
+        testExtract("напомни мне позвонить маме в 0630",
+                    "2017-06-28 06:30:00", "напомни мне позвонить маме")
+        testExtract("напомни мне позвонить маме в 06 30 часов",
+                    "2017-06-28 06:30:00", "напомни мне позвонить маме")
+        testExtract("напомни мне позвонить маме в 06 30",
+                    "2017-06-28 06:30:00", "напомни мне позвонить маме")
+        testExtract("напомни мне позвонить маме в 06 30 часа",
+                    "2017-06-28 06:30:00", "напомни мне позвонить маме")
+        testExtract("напомни мне позвонить маме в 7 часов",
+                    "2017-06-27 19:00:00", "напомни мне позвонить маме")
+        testExtract("напомни мне позвонить маме вечером в 7 часов",
+                    "2017-06-27 19:00:00", "напомни мне позвонить маме")
+        testExtract("напомни мне позвонить маме  в 7 часов вечером",
+                    "2017-06-27 19:00:00", "напомни мне позвонить маме")
+        testExtract("напомни мне позвонить маме в 7 часов утра",
+                    "2017-06-28 07:00:00", "напомни мне позвонить маме")
+        testExtract("напомни мне позвонить маме в четверг вечером в 7 часов",
+                    "2017-06-29 19:00:00", "напомни мне позвонить маме")
+        testExtract("напомни мне позвонить маме в четверг утром в 7 часов",
+                    "2017-06-29 07:00:00", "напомни мне позвонить маме")
+        testExtract("напомни мне позвонить маме в 7 часов в четверг утром",
+                    "2017-06-29 07:00:00", "напомни мне позвонить маме")
+        testExtract("напомни мне позвонить маме в 7:00 в четверг утром",
+                    "2017-06-29 07:00:00", "напомни мне позвонить маме")
+        testExtract("напомни мне позвонить маме в 7:00 в четверг вечером",
+                    "2017-06-29 19:00:00", "напомни мне позвонить маме")
+        testExtract("напомни мне позвонить маме в 8 вечера среды",
+                    "2017-06-28 20:00:00", "напомни мне позвонить маме")
+        testExtract("напомни мне позвонить маме в 8 в среду вечером",
+                    "2017-06-28 20:00:00", "напомни мне позвонить маме")
+        testExtract("напомни мне позвонить маме вечером среды в 8",
+                    "2017-06-28 20:00:00", "напомни мне позвонить маме")
+        testExtract("напомни мне позвонить маме через два часа",
+                    "2017-06-27 15:04:00", "напомни мне позвонить маме")
+        testExtract("напомни мне позвонить маме через 2 часа",
+                    "2017-06-27 15:04:00", "напомни мне позвонить маме")
+        testExtract("напомни мне позвонить маме через 15 минут",
+                    "2017-06-27 13:19:00", "напомни мне позвонить маме")
+        testExtract("напомни мне позвонить маме через пятнадцать минут",
+                    "2017-06-27 13:19:00", "напомни мне позвонить маме")
+        testExtract("напомни мне позвонить маме через пол часа",
+                    "2017-06-27 13:34:00", "напомни мне позвонить маме")
+        testExtract("напомни мне позвонить маме через четверть часа",
+                    "2017-06-27 13:19:00", "напомни мне позвонить маме")
+        # testExtract("напомни мне позвонить маме в 10am на 2 день после этой субботы",
+        #             "2017-07-03 10:00:00", "напомни мне позвонить маме")
+        testExtract("Слушайте музыку Рика Эстли через 2 дня с пятницы",
+                    "2017-07-02 00:00:00", "слушайте музыку рика эстли")
+        testExtract("Начать вторжение в 3:45 pm в четверг",
+                    "2017-06-29 15:45:00", "начать вторжение")
+        testExtract("Начать вторжение в 3:45 вечера в четверг",
+                    "2017-06-29 15:45:00", "начать вторжение")
+        testExtract("Начать вторжение в 3:45 дня в четверг",
+                    "2017-06-29 15:45:00", "начать вторжение")
+        testExtract("В понедельник закажи торт из пекарни",
+                    "2017-07-03 00:00:00", "закажи торт из пекарни")
+        testExtract("Включи музыку с днем рождения через 5 лет",
+                    "2022-06-27 00:00:00", "включи музыку с днем рождения")
+        testExtract("Скайп Маме в 12:45 pm в следующий четверг",
+                    "2017-07-06 12:45:00", "скайп маме")
+        testExtract("Скайп Маме в 12:45 дня в следующий четверг",
+                    "2017-07-06 12:45:00", "скайп маме")
+        testExtract("Какая погода в следующую пятницу?",
+                    "2017-06-30 00:00:00", "какая погода")
+        testExtract("Какая погода в следующую среду?",
+                    "2017-07-05 00:00:00", "какая погода")
+        testExtract("Какая погода в следующий четверг?",
+                    "2017-07-06 00:00:00", "какая погода")
+        testExtract("Какая погода в следующую пятницу утром",
+                    "2017-06-30 08:00:00", "какая погода")
+        testExtract("какая погода в следующую пятницу вечером",
+                    "2017-06-30 19:00:00", "какая погода")
+        testExtract("какая погода в следующую пятницу днём",
+                    "2017-06-30 15:00:00", "какая погода")
+        testExtract("какая погода в следующую пятницу в полдень",
+                    "2017-06-30 12:00:00", "какая погода")
+        testExtract("напомни мне позвонить маме третьего августа",
+                    "2017-08-03 00:00:00", "напомни мне позвонить маме")
+        # testExtract("купить фейерверк в 4 в четверг",
+        #             "2017-07-04 00:00:00", "купить фейерверк")
+        testExtract("какая погода через 2 недели со следующей пятницы",
+                    "2017-07-14 00:00:00", "какая погода")
+        testExtract("какая погода в среду в 0700 часов",
+                    "2017-06-28 07:00:00", "какая погода")
+        testExtract("Поставь будильник в среду в 7 часов",
+                    "2017-06-28 07:00:00", "поставь будильник")
+        testExtract("Назначь встречу в 12:45 pm в следующий четверг",
+                    "2017-07-06 12:45:00", "назначь встречу")
+        testExtract("Назначь встречу в 12:45 дня в следующий четверг",
+                    "2017-07-06 12:45:00", "назначь встречу")
+        testExtract("Какая погода в этот четверг?",
+                    "2017-06-29 00:00:00", "какая погода")
+        testExtract("назначь встречу через 2 недели и 6 дней с субботы",
+                    "2017-07-21 00:00:00", "назначь встречу")
+        testExtract("Начать вторжение в 03 45 в четверг",
+                    "2017-06-29 03:45:00", "начать вторжение")
+        testExtract("Начать вторжение в 800 часов в четверг",
+                    "2017-06-29 08:00:00", "начать вторжение")
+        testExtract("Начать вечеринку в 8 часов вечером в четверг",
+                    "2017-06-29 20:00:00", "начать вечеринку")
+        testExtract("Начать вторжение в 8 вечера в четверг",
+                    "2017-06-29 20:00:00", "начать вторжение")
+        testExtract("Начать вторжение в четверг в полдень",
+                    "2017-06-29 12:00:00", "начать вторжение")
+        testExtract("Начать вторжение в четверг в полночь",
+                    "2017-06-29 00:00:00", "начать вторжение")
+        testExtract("Начать вторжение в четверг в 0500",
+                    "2017-06-29 05:00:00", "начать вторжение")
+        testExtract("напомни мне встать через 4 года",
+                    "2021-06-27 00:00:00", "напомни мне встать")
+        testExtract("напомни мне встать через 4 года и 4 дня",
+                    "2021-07-01 00:00:00", "напомни мне встать")
+        # testExtract("какая погода 3 дня после завтра?",
+        #             "2017-07-01 00:00:00", "какая погода")
+        testExtract("3 декабря",
+                    "2017-12-03 00:00:00", "")
+        testExtract("мы встретимся в 8:00 сегодня вечером",
+                    "2017-06-27 20:00:00", "мы встретимся")
+        testExtract("мы встретимся в 5pm",
+                    "2017-06-27 17:00:00", "мы встретимся")
+        testExtract("мы встретимся в 5дня",
+                    "2017-06-27 17:00:00", "мы встретимся")
+        testExtract("мы встретимся в 8 am",
+                    "2017-06-28 08:00:00", "мы встретимся")
+        testExtract("мы встретимся в 8 утра",
+                    "2017-06-28 08:00:00", "мы встретимся")
+        testExtract("мы встретимся в 8 вечера",
+                    "2017-06-27 20:00:00", "мы встретимся")
+        testExtract("напомнить мне встать в 8 am",
+                    "2017-06-28 08:00:00", "напомнить мне встать")
+        testExtract("напомнить мне встать в 8 утра",
+                    "2017-06-28 08:00:00", "напомнить мне встать")
+        testExtract("какая погода во вторник",
+                    "2017-06-27 00:00:00", "какая погода")
+        testExtract("какая погода в понедельник",
+                    "2017-07-03 00:00:00", "какая погода")
+        testExtract("какая погода в эту среду",
+                    "2017-06-28 00:00:00", "какая погода")
+        testExtract("в четверг какая погода",
+                    "2017-06-29 00:00:00", "какая погода")
+        testExtract("в этот четверг какая погода",
+                    "2017-06-29 00:00:00", "какая погода")
+        testExtract("в прошлый понедельник какая была погода",
+                    "2017-06-26 00:00:00", "какая была погода")
+        testExtract("поставь будильник на среду вечером в 8",
+                    "2017-06-28 20:00:00", "поставь будильник")
+        testExtract("поставь будильник на среду в 3 часа дня",
+                    "2017-06-28 15:00:00", "поставь будильник")
+        testExtract("поставь будильник на среду в 3 часа утра",
+                    "2017-06-28 03:00:00", "поставь будильник")
+        testExtract("поставь будильник на среду утром в 7 часов",
+                    "2017-06-28 07:00:00", "поставь будильник")
+        testExtract("поставь будильник на сегодня в 7 часов",
+                    "2017-06-27 19:00:00", "поставь будильник")
+        testExtract("поставь будильник на этот вечер в 7 часов",
+                    "2017-06-27 19:00:00", "поставь будильник")
+        testExtract("поставь будильник на этот вечер в 7:00",
+                    "2017-06-27 19:00:00", "поставь будильник")
+        # testExtract("поставь будильник этим вечером в 7:00",
+        #             "2017-06-27 19:00:00", "поставь будильник")
+        testExtract("вечером 5 июня 2017 напомни мне позвонить маме",
+                    "2017-06-05 19:00:00", "напомни мне позвонить маме")
+        testExtract("обнови мой календарь утром свидание с юлиусом" +
+                    " 4 марта",
+                    "2018-03-04 08:00:00",
+                    "обнови мой календарь свидание с юлиусом")
+        testExtract("напомни мне позвонить маме в следующий вторник",
+                    "2017-07-04 00:00:00", "напомни мне позвонить маме")
+        # testExtract("напомни мне позвонить маме  3 недели",
+        #             "2017-07-18 00:00:00", "напомни мне позвонить маме")
+        testExtract("напомни мне позвонить маме через 8 недель",
+                    "2017-08-22 00:00:00", "напомни мне позвонить маме")
+        testExtract("напомни мне позвонить маме через 8 недель и 2 дня",
+                    "2017-08-24 00:00:00", "напомни мне позвонить маме")
+        testExtract("напомни мне позвонить маме через 4 дня",
+                    "2017-07-01 00:00:00", "напомни мне позвонить маме")
+        testExtract("напомни мне позвонить маме через 3 месяца",
+                    "2017-09-27 00:00:00", "напомни мне позвонить маме")
+        testExtract("напомни мне позвонить маме через 2 года и 2 дня",
+                    "2019-06-29 00:00:00", "напомни мне позвонить маме")
+        testExtract("напомни мне позвонить маме на следующей неделе",
+                    "2017-07-04 00:00:00", "напомни мне позвонить маме")
+        testExtract("напомни мне позвонить маме в 10am в субботу",
+                    "2017-07-01 10:00:00", "напомни мне позвонить маме")
+        testExtract("напомни мне позвонить маме в 10 утра в субботу",
+                    "2017-07-01 10:00:00", "напомни мне позвонить маме")
+        # testExtract("напомни мне позвонить маме в 10am в эту субботу",
+        #             "2017-07-01 10:00:00", "напомни мне позвонить маме")
+        testExtract("напомни мне позвонить маме в 10 в следующую субботу",
+                    "2017-07-01 10:00:00", "напомни мне позвонить маме")
+        testExtract("напомни мне позвонить маме в 10am в следующую субботу",
+                    "2017-07-01 10:00:00", "напомни мне позвонить маме")
+        testExtract("напомни мне позвонить маме в 10 утра в следующую субботу",
+                    "2017-07-01 10:00:00", "напомни мне позвонить маме")
+        # test yesterday
+        testExtract("какой был день вчера",
+                    "2017-06-26 00:00:00", "какой был день")
+        testExtract("какой был день позавчера",
+                    "2017-06-25 00:00:00", "какой был день")
+        testExtract("я позавтракал вчера в 6",
+                    "2017-06-26 06:00:00", "я позавтракал")
+        testExtract("я позавтракал вчера в 6 am",
+                    "2017-06-26 06:00:00", "я позавтракал")
+        testExtract("я позавтракал вчера в 6 утра",
+                    "2017-06-26 06:00:00", "я позавтракал")
+
+        # Below two tests, ensure that time is picked
+        # even if no am/pm is specified
+        # in case of weekdays/tonight
+
+        testExtract("поставь будильник на 9 в выходные",
+                    "2017-06-27 21:00:00", "поставь будильник выходные")
+        testExtract("на 8 сегодня вечером",
+                    "2017-06-27 20:00:00", "")
+        testExtract("на 8:30pm сегодня вечером",
+                    "2017-06-27 20:30:00", "")
+        testExtract("на 8:30вечера сегодня",
+                    "2017-06-27 20:30:00", "")
+        testExtract("на 8:30 вечера сегодня",
+                    "2017-06-27 20:30:00", "")
+        # Tests a time with ':' & without am/pm
+        testExtract("поставь будильник сегодня вечером на 9:30",
+                    "2017-06-27 21:30:00", "поставь будильник")
+        testExtract("поставь будильник на 9:00 сегодня вечером",
+                    "2017-06-27 21:00:00", "поставь будильник")
+        # Check if it picks intent irrespective of correctness
+        testExtract("поставь будильник в 9 часов сегодня вечером",
+                    "2017-06-27 21:00:00", "поставь будильник")
+        testExtract("напомни мне об игре сегодня вечером в 11:30",
+                    "2017-06-27 23:30:00", "напомни мне об игре")
+        testExtract("поставь будильник в 7:30 на выходных",
+                    "2017-06-27 19:30:00", "поставь будильник на выходных")
+
+        #  "# days <from X/after X>"
+        testExtract("мой день рождения через 2 дня с сегодня",
+                    "2017-06-29 00:00:00", "мой день рождения")
+        testExtract("мой день рождения через 2 дня от сегодня",
+                    "2017-06-29 00:00:00", "мой день рождения")
+        testExtract("мой день рождения через 2 дня с завтра",
+                    "2017-06-30 00:00:00", "мой день рождения")
+        testExtract("мой день рождения через 2 дня от завтра",
+                    "2017-06-30 00:00:00", "мой день рождения")
+        # testExtract("напомни мне позвонить маме в 10am через 2 дня после следующей субботы",
+        #             "2017-07-10 10:00:00", "напомни мне позвонить маме")
+        testExtract("мой день рождения через 2 дня со вчера",
+                    "2017-06-28 00:00:00", "мой день рождения")
+        testExtract("мой день рождения через 2 дня от вчера",
+                    "2017-06-28 00:00:00", "мой день рождения")
+
+        #  "# days ago>"
+        testExtract("мой день рождения был 1 день назад",
+                    "2017-06-26 00:00:00", "мой день рождения был")
+        testExtract("мой день рождения был 2 дня назад",
+                    "2017-06-25 00:00:00", "мой день рождения был")
+        testExtract("мой день рождения был 3 дня назад",
+                    "2017-06-24 00:00:00", "мой день рождения был")
+        testExtract("мой день рождения был 4 дня назад",
+                    "2017-06-23 00:00:00", "мой день рождения был")
+        testExtract("мой день рождения был 5 дней назад",
+                    "2017-06-22 00:00:00", "мой день рождения был")
+        testExtract("встретимся сегодня ночью",
+                    "2017-06-27 22:00:00", "встретимся ночью")
+        testExtract("встретимся позже ночью",
+                    "2017-06-27 22:00:00", "встретимся позже ночью")
+        testExtract("какая будет погода завтра ночью",
+                    "2017-06-28 22:00:00", "какая будет погода ночью")
+        testExtract("какая будет погода в следующий вторник ночью",
+                    "2017-07-04 22:00:00", "какая будет погода ночью")
+
+    def test_extract_ambiguous_time_ru(self):
+        morning = datetime(2017, 6, 27, 8, 1, 2, tzinfo=default_timezone())
+        evening = datetime(2017, 6, 27, 20, 1, 2, tzinfo=default_timezone())
+        noonish = datetime(2017, 6, 27, 12, 1, 2, tzinfo=default_timezone())
+        self.assertEqual(extract_datetime('кормление рыб'), None)
+        self.assertEqual(extract_datetime('день'), None)
+        # self.assertEqual(extract_datetime('сегодня'), None)
+        self.assertEqual(extract_datetime('месяц'), None)
+        self.assertEqual(extract_datetime('год'), None)
+        self.assertEqual(extract_datetime(' '), None)
+        self.assertEqual(
+            extract_datetime('покормить рыб в 10 часов', morning)[0],
+            datetime(2017, 6, 27, 10, 0, 0, tzinfo=default_timezone()))
+        self.assertEqual(
+            extract_datetime('покормить рыб в 10 часов', noonish)[0],
+            datetime(2017, 6, 27, 22, 0, 0, tzinfo=default_timezone()))
+        self.assertEqual(
+            extract_datetime('покормить рыб в 10 часов', evening)[0],
+            datetime(2017, 6, 27, 22, 0, 0, tzinfo=default_timezone()))
+
+    def test_extract_relativedatetime_ru(self):
+        def extractWithFormat(text):
+            date = datetime(2017, 6, 27, 10, 1, 2, tzinfo=default_timezone())
+            [extractedDate, leftover] = extract_datetime(text, date)
+            extractedDate = extractedDate.strftime("%Y-%m-%d %H:%M:%S")
+            return [extractedDate, leftover]
+
+        def testExtract(text, expected_date, expected_leftover):
+            res = extractWithFormat(normalize(text))
+            self.assertEqual(res[0], expected_date, "for=" + text)
+            self.assertEqual(res[1], expected_leftover, "for=" + text)
+
+        testExtract("мы встретимся через 5 минут",
+                    "2017-06-27 10:06:02", "мы встретимся")
+        # testExtract("мы встретимся через 5минут",
+        #             "2017-06-27 10:06:02", "мы встретимся")
+        testExtract("мы встретимся через 5 секунд",
+                    "2017-06-27 10:01:07", "мы встретимся")
+        testExtract("мы встретимся через 1 час",
+                    "2017-06-27 11:01:02", "мы встретимся")
+        testExtract("мы встретимся через 2 часа",
+                    "2017-06-27 12:01:02", "мы встретимся")
+        testExtract("мы встретимся через 1 минуту",
+                    "2017-06-27 10:02:02", "мы встретимся")
+        testExtract("мы встретимся через 1 секунду",
+                    "2017-06-27 10:01:03", "мы встретимся")
+        # testExtract("мы встретимся через 5секунд",
+        #             "2017-06-27 10:01:07", "мы встретимся")
+
+    def test_spaces(self):
+        self.assertEqual(normalize("  вот   это   тест"),
+                         "вот это тест")
+        self.assertEqual(normalize("  вот   это     тест  "),
+                         "вот это тест")
+        self.assertEqual(normalize("  вот   это  один    тест"),
+                         "вот это 1 тест")
+
+    def test_numbers(self):
+        self.assertEqual(normalize("вот это один два три  тест"),
+                         "вот это 1 2 3 тест")
+        self.assertEqual(normalize("  вот это четыре пять шесть  тест"),
+                         "вот это 4 5 6 тест")
+        self.assertEqual(normalize("вот это семь восемь девять тест"),
+                         "вот это 7 8 9 тест")
+        self.assertEqual(normalize("вот это семь восемь девять  тест"),
+                         "вот это 7 8 9 тест")
+        self.assertEqual(normalize("вот это десять одиннадцать двенадцать тест"),
+                         "вот это 10 11 12 тест")
+        self.assertEqual(normalize("вот это тринадцать четырнадцать тест"),
+                         "вот это 13 14 тест")
+        self.assertEqual(normalize("вот это пятнадцать шестнадцать семнадцать"),
+                         "вот это 15 16 17")
+        self.assertEqual(normalize("вот это восемнадцать девятнадцать двадцать"),
+                         "вот это 18 19 20")
+        self.assertEqual(normalize("вот это один девятнадцать двадцать два"),
+                         "вот это 1 19 20 2")
+        self.assertEqual(normalize("вот это один сто"),
+                         "вот это 1 сто")
+        self.assertEqual(normalize("вот это один два двадцать два"),
+                         "вот это 1 2 20 2")
+        self.assertEqual(normalize("вот это один и половина"),
+                         "вот это 1 и половина")
+        self.assertEqual(normalize("вот это один и половина и пять шесть"),
+                         "вот это 1 и половина и 5 6")
+
+    def test_multiple_numbers(self):
+        self.assertEqual(extract_numbers("вот это один два три тест"),
+                         [1.0, 2.0, 3.0])
+        self.assertEqual(extract_numbers("вот это четыре пять шесть тест"),
+                         [4.0, 5.0, 6.0])
+        self.assertEqual(extract_numbers("вот это десять одиннадцать двенадцать тест"),
+                         [10.0, 11.0, 12.0])
+        self.assertEqual(extract_numbers("вот это один двадцать один тест"),
+                         [1.0, 21.0])
+        self.assertEqual(extract_numbers("1 собака, семь свиней, у макдонадьда "
+                                         "была ферма ферма, 3 раза по 5 макарен"),
+                         [1, 7, 3, 5])
+        # self.assertEqual(extract_numbers("два пива для двух медведей"),
+        #                  [2.0, 2.0])
+        self.assertEqual(extract_numbers("двадцать 20 двадцать"),
+                         [20, 20, 20])
+        self.assertEqual(extract_numbers("двадцать 20 22"),
+                         [20.0, 20.0, 22.0])
+        self.assertEqual(extract_numbers("двадцать двадцать два двадцать"),
+                         [20, 22, 20])
+        self.assertEqual(extract_numbers("двадцать 2"),
+                         [22.0])
+        self.assertEqual(extract_numbers("двадцать 20 двадцать 2"),
+                         [20, 20, 22])
+        self.assertEqual(extract_numbers("треть один"),
+                         [1 / 3, 1])
+        self.assertEqual(extract_numbers("третий", ordinals=True), [3])
+        self.assertEqual(extract_numbers("шесть триллионов", short_scale=True),
+                         [6e12])
+        self.assertEqual(extract_numbers("шесть триллионов", short_scale=False),
+                         [6e18])
+        self.assertEqual(extract_numbers("два поросёнка и шесть триллионов бактерий",
+                                         short_scale=True), [2, 6e12])
+        self.assertEqual(extract_numbers("два поросёнка и шесть триллионов бактерий",
+                                         short_scale=False), [2, 6e18])
+        self.assertEqual(extract_numbers("тридцать второй или первый",
+                                         ordinals=True), [32, 1])
+        self.assertEqual(extract_numbers("вот это семь восемь девять и"
+                                         " половина тест"),
+                         [7.0, 8.0, 9.5])
+
+
+if __name__ == "__main__":
+    unittest.main()