def in_words_float(amount, _gender=FEMALE): """ Float in words @param amount: float numeral @type amount: C{float} or C{Decimal} @return: in-words reprsentation of float numeral @rtype: C{unicode} @raise L{uktils.err.InputParameterError}: input parameters' check failed (when amount is not C{float}) @raise ValueError: when ammount is negative """ utils.check_positive(amount) pts = [] # преобразуем целую часть pts.append(sum_string(int(amount), 2, (u"ціла", u"цілі", u"цілих"))) # теперь то, что после запятой remainder = _get_float_remainder(amount) signs = len(str(remainder)) - 1 pts.append(sum_string(int(remainder), 2, FRACTIONS[signs])) return u" ".join(pts)
def grivnas(amount, zero_for_kopeck=False): """ Get string for money @param amount: amount of money @type amount: C{int}, C{long}, C{float} or C{Decimal} @param zero_for_kopeck: If false, then zero kopecks ignored @type zero_for_kopeck: C{bool} @return: in-words representation of money's amount @rtype: C{unicode} @raise L{uktils.err.InputParameterError}: input parameters' check failed (amount neither C{int}, no C{float}) @raise ValueError: amount is negative """ check_positive(amount) pts = [] amount = round(amount, 2) pts.append(sum_string(int(amount), 2, (u"гривня", u"гривні", u"гривень"))) remainder = _get_float_remainder(amount, 2) iremainder = int(remainder) if iremainder != 0 or zero_for_kopeck: # если 3.1, то это 10 копеек, а не одна if iremainder < 10 and len(remainder) == 1: iremainder *= 10 # pts.append(sum_string(iremainder, 2, # (u"копійка", u"копійки", u"копійок"))) pts.append(u"%0.2d %s" % (iremainder, choose_plural(iremainder, (u"копійка", u"копійки", u"копійок")))) return u" ".join(pts)
def choose_plural(amount, variants): """ Choose proper case depending on amount @param amount: amount of objects @type amount: C{int} or C{long} @param variants: variants (forms) of object in such form: (1 object, 2 objects, 5 objects). @type variants: 3-element C{sequence} of C{unicode} or C{unicode} (three variants with delimeter ',') @return: proper variant @rtype: C{unicode} @raise L{uktils.err.InputParameterError}: input parameters' check failed (amount isn't C{int}, variants isn't C{sequence}) @raise ValueError: amount is negative @raise ValueError: variants' length lesser than 3 """ if isinstance(variants, unicode): variants = utils.split_values(variants) check_length(variants, 3) check_positive(amount) if amount % 10 == 1 and amount % 100 != 11: variant = 0 elif amount % 10 >= 2 and amount % 10 <= 4 and (amount % 100 < 10 or amount % 100 >= 20): variant = 1 else: variant = 2 return variants[variant]
def sum_string(amount, gender, items=None): """ Get sum in words @param amount: amount of objects @type amount: C{int} or C{long} @param gender: gender of object (MALE, FEMALE or NEUTER) @type gender: C{int} @param items: variants of object in three forms: for one object, for two objects and for five objects @type items: 3-element C{sequence} of C{unicode} or just C{unicode} (three variants with delimeter ',') @return: in-words representation objects' amount @rtype: C{unicode} @raise L{uktils.err.InputParameterError}: input parameters' check failed @raise ValueError: items isn't 3-element C{sequence} or C{unicode} @raise ValueError: amount bigger than 10**11 @raise ValueError: amount is negative """ if isinstance(items, unicode): items = utils.split_values(items) if items is None: items = (u"", u"", u"") try: one_item, two_items, five_items = items except ValueError: raise ValueError("Items must be 3-element sequence") check_positive(amount) if amount == 0: return u"нуль %s" % five_items into = u"" tmp_val = amount # единицы into, tmp_val = _sum_string_fn(into, tmp_val, gender, items) # тысячи into, tmp_val = _sum_string_fn(into, tmp_val, FEMALE, (u"тисяча", u"тисячі", u"тисяч")) # миллионы into, tmp_val = _sum_string_fn(into, tmp_val, MALE, (u"мільйон", u"мільйона", u"мільйонів")) # миллиарды into, tmp_val = _sum_string_fn(into, tmp_val, MALE, (u"мільйард", u"мільйарда", u"мільйардів")) if tmp_val == 0: return into else: raise ValueError("Cannot operand with numbers bigger than 10**11")
def _get_float_remainder(fvalue, signs=9): """ Get remainder of float, i.e. 2.05 -> '05' @param fvalue: input value @type fvalue: C{int}, C{long}, C{float} or C{Decimal} @param signs: maximum number of signs @type signs: C{int} or C{long} @return: remainder @rtype: C{str} @raise L{uktils.err.InputParameterError}: input parameters' check failed (fvalue neither C{int}, no C{float}) @raise ValueError: fvalue is negative @raise ValueError: signs overflow """ check_positive(fvalue) if isinstance(fvalue, (int, long)): return "0" if isinstance(fvalue, Decimal) and fvalue.as_tuple()[2] == 0: # Decimal.as_tuple() -> (sign, digit_tuple, exponent) # если экспонента "0" -- значит дробной части нет return "0" signs = min(signs, len(FRACTIONS)) # нужно remainder в строке, потому что дробные X.0Y # будут "ломаться" до X.Y remainder = str(fvalue).split(".")[1] iremainder = int(remainder) orig_remainder = remainder factor = len(str(remainder)) - signs if factor > 0: # после запятой цифр больше чем signs, округляем iremainder = int(round(iremainder / (10.0 ** factor))) format = "%%0%dd" % min(len(remainder), signs) remainder = format % iremainder if len(remainder) > signs: # при округлении цифр вида 0.998 ругаться raise ValueError( "Signs overflow: I can't round only fractional part \ of %s to fit %s in %d signs" % (str(fvalue), orig_remainder, signs) ) return remainder
def in_words_int(amount, gender=MALE): """ Integer in words @param amount: numeral @type amount: C{int} or C{long} @param gender: gender (MALE, FEMALE or NEUTER) @type gender: C{int} @return: in-words reprsentation of numeral @rtype: C{unicode} @raise L{uktils.err.InputParameterError}: input parameters' check failed (when amount is not C{int}) @raise ValueError: amount is negative """ check_positive(amount) return sum_string(amount, gender)
def in_words(amount, gender=None): """ Numeral in words @param amount: numeral @type amount: C{int}, C{long}, C{float} or C{Decimal} @param gender: gender (MALE, FEMALE or NEUTER) @type gender: C{int} @return: in-words reprsentation of numeral @rtype: C{unicode} raise L{uktils.err.InputParameterError}: input parameters' check failed (when amount not C{int} or C{float}, gender is not C{int} (and not None), gender isn't in (MALE, FEMALE, NEUTER)) raise ValueError: when amount is negative """ check_positive(amount) if isinstance(amount, Decimal) and amount.as_tuple()[2] == 0: # если целое, # т.е. Decimal.as_tuple -> (sign, digits tuple, exponent), exponent=0 # то как целое amount = int(amount) if gender is None: args = (amount,) else: args = (amount, gender) # если целое if isinstance(amount, (int, long)): return in_words_int(*args) # если дробное elif isinstance(amount, (float, Decimal)): return in_words_float(*args) # ни float, ни int, ни Decimal else: # до сюда не должно дойти raise RuntimeError()
def _sum_string_fn(into, tmp_val, gender, items=None): """ Make in-words representation of single order @param into: in-words representation of lower orders @type into: C{unicode} @param tmp_val: temporary value without lower orders @type tmp_val: C{int} or C{long} @param gender: gender (MALE, FEMALE or NEUTER) @type gender: C{int} @param items: variants of objects @type items: 3-element C{sequence} of C{unicode} @return: new into and tmp_val @rtype: C{tuple} @raise L{uktils.err.InputParameterError}: input parameters' check failed @raise ValueError: tmp_val is negative """ if items is None: items = (u"", u"", u"") one_item, two_items, five_items = items check_positive(tmp_val) if tmp_val == 0: return into, tmp_val words = [] rest = tmp_val % 1000 tmp_val = tmp_val / 1000 if rest == 0: # последние три знака нулевые if into == u"": into = u"%s " % five_items return into, tmp_val # начинаем подсчет с rest end_word = five_items # сотни words.append(HUNDREDS[rest / 100]) # десятки rest = rest % 100 rest1 = rest / 10 # особый случай -- tens=1 tens = rest1 == 1 and TENS[rest] or TENS[rest1] words.append(tens) # единицы if rest1 < 1 or rest1 > 1: amount = rest % 10 end_word = choose_plural(amount, items) words.append(ONES[amount][gender - 1]) words.append(end_word) # добавляем то, что уже было words.append(into) # убираем пустые подстроки words = filter(lambda x: len(x) > 0, words) # склеиваем и отдаем return u" ".join(words).strip(), tmp_val
def distance_of_time_in_words(from_time, accuracy=1, to_time=None): """ Represents distance of time in words @param from_time: source time (in seconds from epoch) @type from_time: C{int}, C{float} or C{datetime.datetime} @param accuracy: level of accuracy (1..3), default=1 @type accuracy: C{int} @param to_time: target time (in seconds from epoch), default=None translates to current time @type to_time: C{int}, C{float} or C{datetime.datetime} @return: distance of time in words @rtype: unicode @raise L{uktils.err.InputParameterError}: input parameters' check failed @raise ValueError: accuracy is lesser or equal zero """ current = False if to_time is None: current = True to_time = datetime.datetime.now() check_positive(accuracy, strict=True) if not isinstance(from_time, datetime.datetime): from_time = datetime.datetime.fromtimestamp(from_time) if not isinstance(to_time, datetime.datetime): to_time = datetime.datetime.fromtimestamp(to_time) dt_delta = to_time - from_time difference = dt_delta.days*86400 + dt_delta.seconds minutes_orig = int(abs(difference)/60.0) hours_orig = int(abs(difference)/3600.0) days_orig = int(abs(difference)/86400.0) in_future = from_time > to_time words = [] values = [] alternatives = [] days = days_orig hours = hours_orig - days_orig*24 words.append(u"%d %s" % (days, numeral.choose_plural(days, DAY_VARIANTS))) values.append(days) words.append(u"%d %s" % \ (hours, numeral.choose_plural(hours, HOUR_VARIANTS))) values.append(hours) hours == 1 and current and alternatives.append(u"годину") minutes = minutes_orig - hours_orig*60 words.append(u"%d %s" % (minutes, numeral.choose_plural(minutes, MINUTE_VARIANTS))) values.append(minutes) minutes == 1 and current and alternatives.append(u"хвилину") # убираем из values и words конечные нули while values and not values[-1]: values.pop() words.pop() # убираем из values и words начальные нули while values and not values[0]: values.pop(0) words.pop(0) limit = min(accuracy, len(words)) real_words = words[:limit] real_values = values[:limit] # снова убираем конечные нули while real_values and not real_values[-1]: real_values.pop() real_words.pop() limit -= 1 real_str = u" ".join(real_words) # альтернативные варианты нужны только если в real_words одно значение # и, вдобавок, если используется текущее время alter_str = limit == 1 and current and alternatives and \ not (days and hours==1) and \ not (hours and minutes==1) and \ alternatives[0] _result_str = alter_str or real_str result_str = in_future and u"%s %s" % (PREFIX_IN, _result_str) \ or u"%s %s" % (_result_str, SUFFIX_AGO) # если же прошло менее минуты, то real_words -- пустой, и поэтому # нужно брать alternatives[0], а не result_str zero_str = minutes == 0 and not real_words and \ (in_future and u"менш ніж через хвилину" \ or u"менше хвилини тому") # нужно использовать вчера/позавчера/завтра/послезавтра # если days 1..2 и в real_words одно значение day_alternatives = DAY_ALTERNATIVES.get(days, False) alternate_day = day_alternatives and current and limit == 1 and \ ((in_future and day_alternatives[1]) \ or day_alternatives[0]) final_str = not real_words and zero_str or alternate_day or result_str return final_str