diff --git a/arrow/arrow.py b/arrow/arrow.py index aff35086e..1bb219236 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -904,7 +904,12 @@ def humanize( dt = other.astimezone(self._datetime.tzinfo) else: - raise TypeError() + raise TypeError( + "Invalid 'other' argument of type '{}'. " + "Argument must be of type None, Arrow, or datetime.".format( + type(other).__name__ + ) + ) if isinstance(granularity, list) and len(granularity) == 1: granularity = granularity[0] @@ -939,7 +944,8 @@ def humanize( hours = sign * int(max(delta / 3600, 2)) return locale.describe("hours", hours, only_distance=only_distance) - elif diff < 129600: + # anything less than 48 hours should be 1 day + elif diff < 172800: return locale.describe("day", sign, only_distance=only_distance) elif diff < 554400: days = sign * int(max(delta / 86400, 2)) @@ -1033,17 +1039,20 @@ def humanize( if len(timeframes) < len(granularity): raise AttributeError( - "Invalid level of granularity. Please select between 'second', 'minute', 'hour', 'day', 'week', 'month' or 'year'" + "Invalid level of granularity. " + "Please select between 'second', 'minute', 'hour', 'day', 'week', 'month' or 'year'." ) - for index, (_, delta) in enumerate(timeframes): - if trunc(abs(delta)) != 1: - timeframes[index][0] += "s" + for tf in timeframes: + # Make granularity plural if the delta is not equal to 1 + if trunc(abs(tf[1])) != 1: + tf[0] += "s" return locale.describe_multi(timeframes, only_distance=only_distance) except KeyError as e: raise ValueError( - "Humanization of the {} granularity is not currently translated in the '{}' locale. Please consider making a contribution to this locale.".format( + "Humanization of the {} granularity is not currently translated in the '{}' locale. " + "Please consider making a contribution to this locale.".format( e, locale_name ) ) diff --git a/arrow/locales.py b/arrow/locales.py index 2c12cd964..a42947f66 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -32,6 +32,7 @@ class Locale(object): timeframes = { "now": "", + "second": "", "seconds": "", "minute": "", "minutes": "", @@ -83,7 +84,7 @@ def describe_multi(self, timeframes, only_distance=False): """ Describes a delta within multiple timeframes in plain language. :param timeframes: a list of string, quantity pairs each representing a timeframe and delta. - :param only_distance: return only distance eg: "2 hours 11 seconds" without "in" or "ago" keywords + :param only_distance: return only distance eg: "2 hours and 11 seconds" without "in" or "ago" keywords """ humanized = "" @@ -225,7 +226,8 @@ class EnglishLocale(Locale): timeframes = { "now": "just now", - "seconds": "seconds", + "second": "a second", + "seconds": "{0} seconds", "minute": "a minute", "minutes": "{0} minutes", "hour": "an hour", @@ -321,7 +323,8 @@ class ItalianLocale(Locale): timeframes = { "now": "adesso", - "seconds": "qualche secondo", + "second": "un secondo", + "seconds": "{0} qualche secondo", "minute": "un minuto", "minutes": "{0} minuti", "hour": "un'ora", @@ -391,7 +394,8 @@ class SpanishLocale(Locale): timeframes = { "now": "ahora", - "seconds": "segundos", + "second": "un segundo", + "seconds": "{0} segundos", "minute": "un minuto", "minutes": "{0} minutos", "hour": "una hora", @@ -465,7 +469,8 @@ class FrenchLocale(Locale): timeframes = { "now": "maintenant", - "seconds": "quelques secondes", + "second": "une seconde", + "seconds": "{0} quelques secondes", "minute": "une minute", "minutes": "{0} minutes", "hour": "une heure", @@ -543,7 +548,8 @@ class GreekLocale(Locale): timeframes = { "now": "τώρα", - "seconds": "δευτερόλεπτα", + "second": "ένα δεύτερο", + "seconds": "{0} δευτερόλεπτα", "minute": "ένα λεπτό", "minutes": "{0} λεπτά", "hour": "μία ώρα", @@ -609,7 +615,8 @@ class JapaneseLocale(Locale): timeframes = { "now": "現在", - "seconds": "数秒", + "second": "二番目の", + "seconds": "{0}数秒", "minute": "1分", "minutes": "{0}分", "hour": "1時間", @@ -669,7 +676,8 @@ class SwedishLocale(Locale): timeframes = { "now": "just nu", - "seconds": "några sekunder", + "second": "en sekund", + "seconds": "{0} några sekunder", "minute": "en minut", "minutes": "{0} minuter", "hour": "en timme", @@ -738,7 +746,8 @@ class FinnishLocale(Locale): timeframes = { "now": ["juuri nyt", "juuri nyt"], - "seconds": ["muutama sekunti", "muutaman sekunnin"], + "second": ["sekunti", "sekunti"], + "seconds": ["{0} muutama sekunti", "{0} muutaman sekunnin"], "minute": ["minuutti", "minuutin"], "minutes": ["{0} minuuttia", "{0} minuutin"], "hour": ["tunti", "tunnin"], @@ -825,7 +834,8 @@ class ChineseCNLocale(Locale): timeframes = { "now": "刚才", - "seconds": "几秒", + "second": "一秒", + "seconds": "{0}几秒", "minute": "1分钟", "minutes": "{0}分钟", "hour": "1小时", @@ -884,7 +894,8 @@ class ChineseTWLocale(Locale): timeframes = { "now": "剛才", - "seconds": "幾秒", + "second": "一秒", + "seconds": "{0}幾秒", "minute": "1分鐘", "minutes": "{0}分鐘", "hour": "1小時", @@ -1001,7 +1012,8 @@ class KoreanLocale(Locale): timeframes = { "now": "지금", - "seconds": "몇 초", + "second": "두 번째", + "seconds": "{0}몇 초", "minute": "1분", "minutes": "{0}분", "hour": "1시간", @@ -1059,7 +1071,8 @@ class DutchLocale(Locale): timeframes = { "now": "nu", - "seconds": "seconden", + "second": "een seconde", + "seconds": "{0} seconden", "minute": "een minuut", "minutes": "{0} minuten", "hour": "een uur", @@ -1147,7 +1160,8 @@ class BelarusianLocale(SlavicBaseLocale): timeframes = { "now": "зараз", - "seconds": "некалькі секунд", + "second": "секунду", + "seconds": "{0} некалькі секунд", "minute": "хвіліну", "minutes": ["{0} хвіліну", "{0} хвіліны", "{0} хвілін"], "hour": "гадзіну", @@ -1213,7 +1227,8 @@ class PolishLocale(SlavicBaseLocale): timeframes = { "now": "teraz", - "seconds": "kilka sekund", + "second": "sekunda", + "seconds": "{0} kilka sekund", "minute": "minutę", "minutes": ["{0} minut", "{0} minuty", "{0} minut"], "hour": "godzina", @@ -1279,7 +1294,8 @@ class RussianLocale(SlavicBaseLocale): timeframes = { "now": "сейчас", - "seconds": "несколько секунд", + "second": "Второй", + "seconds": "{0} несколько секунд", "minute": "минуту", "minutes": ["{0} минуту", "{0} минуты", "{0} минут"], "hour": "час", @@ -1347,7 +1363,8 @@ class AfrikaansLocale(Locale): timeframes = { "now": "nou", - "seconds": "sekondes", + "second": "n sekonde", + "seconds": "{0} sekondes", "minute": "minuut", "minutes": "{0} minute", "hour": "uur", @@ -1413,7 +1430,8 @@ class BulgarianLocale(SlavicBaseLocale): timeframes = { "now": "сега", - "seconds": "няколко секунди", + "second": "секунда", + "seconds": "{0} няколко секунди", "minute": "минута", "minutes": ["{0} минута", "{0} минути", "{0} минути"], "hour": "час", @@ -1479,7 +1497,8 @@ class UkrainianLocale(SlavicBaseLocale): timeframes = { "now": "зараз", - "seconds": "кілька секунд", + "second": "секунда", + "seconds": "{0} кілька секунд", "minute": "хвилину", "minutes": ["{0} хвилину", "{0} хвилини", "{0} хвилин"], "hour": "годину", @@ -1544,7 +1563,8 @@ class MacedonianLocale(SlavicBaseLocale): timeframes = { "now": "сега", - "seconds": "секунди", + "second": "секунда", + "seconds": "{0} секунди", "minute": "една минута", "minutes": ["{0} минута", "{0} минути", "{0} минути"], "hour": "еден саат", @@ -1620,7 +1640,8 @@ class DeutschBaseLocale(Locale): timeframes = { "now": "gerade eben", - "seconds": "Sekunden", + "second": "eine Sekunde", + "seconds": "{0} Sekunden", "minute": "einer Minute", "minutes": "{0} Minuten", "hour": "einer Stunde", @@ -1740,7 +1761,8 @@ class NorwegianLocale(Locale): timeframes = { "now": "nå nettopp", - "seconds": "noen sekunder", + "second": "et sekund", + "seconds": "{0} noen sekunder", "minute": "ett minutt", "minutes": "{0} minutter", "hour": "en time", @@ -1806,7 +1828,8 @@ class NewNorwegianLocale(Locale): timeframes = { "now": "no nettopp", - "seconds": "nokre sekund", + "second": "et sekund", + "seconds": "{0} nokre sekund", "minute": "ett minutt", "minutes": "{0} minutt", "hour": "ein time", @@ -2010,7 +2033,8 @@ class TagalogLocale(Locale): timeframes = { "now": "ngayon lang", - "seconds": "segundo", + "second": "isang segundo", + "seconds": "{0} segundo", "minute": "isang minuto", "minutes": "{0} minuto", "hour": "isang oras", @@ -2079,7 +2103,8 @@ class VietnameseLocale(Locale): timeframes = { "now": "hiện tại", - "seconds": "giây", + "second": "một giây", + "seconds": "{0} giây", "minute": "một phút", "minutes": "{0} phút", "hour": "một giờ", @@ -2147,7 +2172,8 @@ class TurkishLocale(Locale): timeframes = { "now": "şimdi", - "seconds": "saniye", + "second": "bir saniye", + "seconds": "{0} saniye", "minute": "bir dakika", "minutes": "{0} dakika", "hour": "bir saat", @@ -2213,7 +2239,8 @@ class AzerbaijaniLocale(Locale): timeframes = { "now": "indi", - "seconds": "saniyə", + "second": "saniyə", + "seconds": "{0} saniyə", "minute": "bir dəqiqə", "minutes": "{0} dəqiqə", "hour": "bir saat", @@ -2297,6 +2324,7 @@ class ArabicLocale(Locale): timeframes = { "now": "الآن", + "second": "ثانية", "seconds": {"double": "ثانيتين", "ten": "{0} ثوان", "higher": "{0} ثانية"}, "minute": "دقيقة", "minutes": {"double": "دقيقتين", "ten": "{0} دقائق", "higher": "{0} دقيقة"}, @@ -2521,7 +2549,8 @@ def _format_timeframe(self, timeframe, delta): timeframes = { "now": "rétt í þessu", - "seconds": ("nokkrum sekúndum", "nokkrar sekúndur"), + "second": ("sekúndu", "sekúndu"), + "seconds": ("{0} nokkrum sekúndum", "nokkrar sekúndur"), "minute": ("einni mínútu", "eina mínútu"), "minutes": ("{0} mínútum", "{0} mínútur"), "hour": ("einum tíma", "einn tíma"), @@ -2590,7 +2619,8 @@ class DanishLocale(Locale): timeframes = { "now": "lige nu", - "seconds": "et par sekunder", + "second": "et sekund", + "seconds": "{0} et par sekunder", "minute": "et minut", "minutes": "{0} minutter", "hour": "en time", @@ -2656,7 +2686,8 @@ class MalayalamLocale(Locale): timeframes = { "now": "ഇപ്പോൾ", - "seconds": "സെക്കന്റ്‌", + "second": "ഒരു നിമിഷം", + "seconds": "{0} സെക്കന്റ്‌", "minute": "ഒരു മിനിറ്റ്", "minutes": "{0} മിനിറ്റ്", "hour": "ഒരു മണിക്കൂർ", @@ -2729,7 +2760,8 @@ class HindiLocale(Locale): timeframes = { "now": "अभी", - "seconds": "सेकंड्", + "second": "एक पल", + "seconds": "{0} सेकंड्", "minute": "एक मिनट ", "minutes": "{0} मिनट ", "hour": "एक घंटा", @@ -2793,6 +2825,7 @@ class CzechLocale(Locale): timeframes = { "now": "Teď", + "second": {"past": "vteřina", "future": "vteřina", "zero": "vteřina"}, "seconds": {"past": "{0} sekundami", "future": ["{0} sekundy", "{0} sekund"]}, "minute": {"past": "minutou", "future": "minutu", "zero": "{0} minut"}, "minutes": {"past": "{0} minutami", "future": ["{0} minuty", "{0} minut"]}, @@ -2879,6 +2912,7 @@ class SlovakLocale(Locale): timeframes = { "now": "Teraz", + "second": {"past": "druhý", "future": "druhý", "zero": "druhý"}, "seconds": {"past": "pár sekundami", "future": ["{0} sekundy", "{0} sekúnd"]}, "minute": {"past": "minútou", "future": "minútu", "zero": "{0} minút"}, "minutes": {"past": "{0} minútami", "future": ["{0} minúty", "{0} minút"]}, @@ -2970,7 +3004,8 @@ class FarsiLocale(Locale): timeframes = { "now": "اکنون", - "seconds": "ثانیه", + "second": "یک لحظه", + "seconds": "{0} ثانیه", "minute": "یک دقیقه", "minutes": "{0} دقیقه", "hour": "یک ساعت", @@ -3043,7 +3078,8 @@ class HebrewLocale(Locale): timeframes = { "now": "הרגע", - "seconds": "שניות", + "second": "שנייה", + "seconds": "{0} שניות", "minute": "דקה", "minutes": "{0} דקות", "hour": "שעה", @@ -3119,7 +3155,8 @@ class MarathiLocale(Locale): timeframes = { "now": "सद्य", - "seconds": "सेकंद", + "second": "एक सेकंद", + "seconds": "{0} सेकंद", "minute": "एक मिनिट ", "minutes": "{0} मिनिट ", "hour": "एक तास", @@ -3198,7 +3235,8 @@ class CatalanLocale(Locale): timeframes = { "now": "Ara mateix", - "seconds": "segons", + "second": "un segon", + "seconds": "{0} segons", "minute": "1 minut", "minutes": "{0} minuts", "hour": "una hora", @@ -3270,7 +3308,8 @@ class BasqueLocale(Locale): timeframes = { "now": "Orain", - "seconds": "segundu", + "second": "segundo bat", + "seconds": "{0} segundu", "minute": "minutu bat", "minutes": "{0} minutu", "hour": "ordu bat", @@ -3335,7 +3374,8 @@ class HungarianLocale(Locale): timeframes = { "now": "éppen most", - "seconds": {"past": "másodpercekkel", "future": "pár másodperc"}, + "second": {"past": "egy második", "future": "egy második"}, + "seconds": {"past": "{0} másodpercekkel", "future": "{0} pár másodperc"}, "minute": {"past": "egy perccel", "future": "egy perc"}, "minutes": {"past": "{0} perccel", "future": "{0} perc"}, "hour": {"past": "egy órával", "future": "egy óra"}, @@ -3412,7 +3452,8 @@ class EsperantoLocale(Locale): timeframes = { "now": "nun", - "seconds": "kelkaj sekundoj", + "second": "sekundo", + "seconds": "{0} kelkaj sekundoj", "minute": "unu minuto", "minutes": "{0} minutoj", "hour": "un horo", @@ -3485,7 +3526,8 @@ class ThaiLocale(Locale): timeframes = { "now": "ขณะนี้", - "seconds": "ไม่กี่วินาที", + "second": "วินาที", + "seconds": "{0} ไม่กี่วินาที", "minute": "1 นาที", "minutes": "{0} นาที", "hour": "1 ชั่วโมง", @@ -3565,7 +3607,8 @@ class BengaliLocale(Locale): timeframes = { "now": "এখন", - "seconds": "সেকেন্ড", + "second": "একটি দ্বিতীয়", + "seconds": "{0} সেকেন্ড", "minute": "এক মিনিট", "minutes": "{0} মিনিট", "hour": "এক ঘণ্টা", @@ -3645,7 +3688,8 @@ class RomanshLocale(Locale): timeframes = { "now": "en quest mument", - "seconds": "secundas", + "second": "in secunda", + "seconds": "{0} secundas", "minute": "ina minuta", "minutes": "{0} minutas", "hour": "in'ura", @@ -3713,7 +3757,8 @@ class SwissLocale(Locale): timeframes = { "now": "gerade eben", - "seconds": "Sekunden", + "second": "eine Sekunde", + "seconds": "{0} Sekunden", "minute": "einer Minute", "minutes": "{0} Minuten", "hour": "einer Stunde", @@ -3783,7 +3828,8 @@ class RomanianLocale(Locale): timeframes = { "now": "acum", - "seconds": "câteva secunde", + "second": "o secunda", + "seconds": "{0} câteva secunde", "minute": "un minut", "minutes": "{0} minute", "hour": "o oră", @@ -3849,7 +3895,8 @@ class SlovenianLocale(Locale): timeframes = { "now": "zdaj", - "seconds": "sekund", + "second": "sekundo", + "seconds": "{0} sekund", "minute": "minuta", "minutes": "{0} minutami", "hour": "uro", @@ -3920,7 +3967,8 @@ class IndonesianLocale(Locale): timeframes = { "now": "baru saja", - "seconds": "detik", + "second": "1 sebentar", + "seconds": "{0} detik", "minute": "1 menit", "minutes": "{0} menit", "hour": "1 jam", @@ -3989,7 +4037,8 @@ class NepaliLocale(Locale): timeframes = { "now": "अहिले", - "seconds": "सेकण्ड", + "second": "एक सेकेन्ड", + "seconds": "{0} सेकण्ड", "minute": "मिनेट", "minutes": "{0} मिनेट", "hour": "एक घण्टा", diff --git a/tests/arrow_tests.py b/tests/arrow_tests.py index c92f78a80..9d6827660 100644 --- a/tests/arrow_tests.py +++ b/tests/arrow_tests.py @@ -1544,10 +1544,10 @@ def test_granularity(self): later100 = self.now.shift(seconds=100) self.assertEqual( - self.now.humanize(later100, granularity="second"), "seconds ago" + self.now.humanize(later100, granularity="second"), "100 seconds ago" ) self.assertEqual( - later100.humanize(self.now, granularity="second"), "in seconds" + later100.humanize(self.now, granularity="second"), "in 100 seconds" ) self.assertEqual( self.now.humanize(later100, granularity="minute"), "a minute ago" @@ -1646,7 +1646,7 @@ def test_multiple_granularity(self): self.assertEqual(self.now.humanize(granularity=["second"]), "just now") self.assertEqual( self.now.humanize(granularity=["year", "month", "day", "hour", "second"]), - "in 0 years 0 months 0 days 0 hours and seconds", + "in 0 years 0 months 0 days 0 hours and 0 seconds", ) later4000 = self.now.shift(seconds=4000) @@ -1698,18 +1698,31 @@ def test_multiple_granularity(self): self.now.humanize( later108onlydistance, only_distance=True, granularity=["year", "second"] ), - "3 years and seconds", + "3 years and 5327200 seconds", + ) + + one_min_one_sec_ago = self.now.shift(minutes=-1, seconds=-1) + self.assertEqual( + one_min_one_sec_ago.humanize(self.now, granularity=["minute", "second"]), + "a minute and a second ago", + ) + + one_min_two_secs_ago = self.now.shift(minutes=-1, seconds=-2) + self.assertEqual( + one_min_two_secs_ago.humanize(self.now, granularity=["minute", "second"]), + "a minute and 2 seconds ago", ) def test_seconds(self): later = self.now.shift(seconds=10) - self.assertEqual(self.now.humanize(later), "seconds ago") - self.assertEqual(later.humanize(self.now), "in seconds") + # regression test for issue #727 + self.assertEqual(self.now.humanize(later), "10 seconds ago") + self.assertEqual(later.humanize(self.now), "in 10 seconds") - self.assertEqual(self.now.humanize(later, only_distance=True), "seconds") - self.assertEqual(later.humanize(self.now, only_distance=True), "seconds") + self.assertEqual(self.now.humanize(later, only_distance=True), "10 seconds") + self.assertEqual(later.humanize(self.now, only_distance=True), "10 seconds") def test_minute(self): @@ -1758,6 +1771,23 @@ def test_day(self): self.assertEqual(self.now.humanize(later), "a day ago") self.assertEqual(later.humanize(self.now), "in a day") + # regression test for issue #697 + less_than_48_hours = self.now.shift( + days=1, hours=23, seconds=59, microseconds=999999 + ) + self.assertEqual(self.now.humanize(less_than_48_hours), "a day ago") + self.assertEqual(less_than_48_hours.humanize(self.now), "in a day") + + less_than_48_hours_date = less_than_48_hours._datetime.date() + with self.assertRaises(TypeError): + # humanize other argument does not take raw datetime.date objects + self.now.humanize(less_than_48_hours_date) + + # convert from date to arrow object + less_than_48_hours_date = arrow.Arrow.fromdate(less_than_48_hours_date) + self.assertEqual(self.now.humanize(less_than_48_hours_date), "a day ago") + self.assertEqual(less_than_48_hours_date.humanize(self.now), "in a day") + self.assertEqual(self.now.humanize(later, only_distance=True), "a day") self.assertEqual(later.humanize(self.now, only_distance=True), "a day") @@ -1922,7 +1952,7 @@ def test_seconds(self): result = arw.humanize(self.datetime, locale="ru") - self.assertEqual(result, "через несколько секунд") + self.assertEqual(result, "через 44 несколько секунд") def test_years(self): diff --git a/tests/locales_tests.py b/tests/locales_tests.py index 83e56645a..af205b435 100644 --- a/tests/locales_tests.py +++ b/tests/locales_tests.py @@ -153,9 +153,9 @@ def test_ordinal_number(self): def test_format_timeframe(self): locale = locales.SpanishLocale() self.assertEqual(locale._format_timeframe("now", 0), "ahora") - self.assertEqual(locale._format_timeframe("seconds", 1), "segundos") - self.assertEqual(locale._format_timeframe("seconds", 3), "segundos") - self.assertEqual(locale._format_timeframe("seconds", 30), "segundos") + self.assertEqual(locale._format_timeframe("seconds", 1), "1 segundos") + self.assertEqual(locale._format_timeframe("seconds", 3), "3 segundos") + self.assertEqual(locale._format_timeframe("seconds", 30), "30 segundos") self.assertEqual(locale._format_timeframe("minute", 1), "un minuto") self.assertEqual(locale._format_timeframe("minutes", 4), "4 minutos") self.assertEqual(locale._format_timeframe("minutes", 40), "40 minutos") @@ -176,9 +176,9 @@ def test_format_timeframe(self): self.assertEqual(locale._format_timeframe("years", 12), "12 años") self.assertEqual(locale._format_timeframe("now", 0), "ahora") - self.assertEqual(locale._format_timeframe("seconds", -1), "segundos") - self.assertEqual(locale._format_timeframe("seconds", -9), "segundos") - self.assertEqual(locale._format_timeframe("seconds", -12), "segundos") + self.assertEqual(locale._format_timeframe("seconds", -1), "1 segundos") + self.assertEqual(locale._format_timeframe("seconds", -9), "9 segundos") + self.assertEqual(locale._format_timeframe("seconds", -12), "12 segundos") self.assertEqual(locale._format_timeframe("minute", -1), "un minuto") self.assertEqual(locale._format_timeframe("minutes", -2), "2 minutos") self.assertEqual(locale._format_timeframe("minutes", -10), "10 minutos") @@ -805,7 +805,7 @@ def test_format_timeframe(self): self.assertEqual(self.locale._format_timeframe("month", 1), "isang buwan") self.assertEqual(self.locale._format_timeframe("year", 1), "isang taon") - self.assertEqual(self.locale._format_timeframe("seconds", 2), "segundo") + self.assertEqual(self.locale._format_timeframe("seconds", 2), "2 segundo") self.assertEqual(self.locale._format_timeframe("minutes", 3), "3 minuto") self.assertEqual(self.locale._format_timeframe("hours", 4), "4 oras") self.assertEqual(self.locale._format_timeframe("months", 5), "5 buwan")