diff --git a/numbers/src/main/java/org/dicio/numbers/lang/sv/SwedishDateTimeExtractor.kt b/numbers/src/main/java/org/dicio/numbers/lang/sv/SwedishDateTimeExtractor.kt new file mode 100644 index 00000000..a9826f44 --- /dev/null +++ b/numbers/src/main/java/org/dicio/numbers/lang/sv/SwedishDateTimeExtractor.kt @@ -0,0 +1,415 @@ +package org.dicio.numbers.lang.sv + +import org.dicio.numbers.parser.lexer.TokenStream +import org.dicio.numbers.unit.Duration +import org.dicio.numbers.util.DateTimeExtractorUtils +import org.dicio.numbers.util.DurationExtractorUtils +import org.dicio.numbers.util.NumberExtractorUtils +import org.dicio.numbers.util.Utils +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime +import java.time.temporal.ChronoUnit + +class SwedishDateTimeExtractor internal constructor( + private val ts: TokenStream, + shortScale: Boolean, + private val preferMonthBeforeDay: Boolean, + private val now: LocalDateTime +) { + private val numberExtractor = SwedishNumberExtractor(ts, shortScale) + private val durationExtractor = DurationExtractorUtils(ts, numberExtractor::numberNoOrdinal) + private val dateTimeExtractor = DateTimeExtractorUtils(ts, now, this::extractIntegerInRange) + + private fun extractIntegerInRange( + fromInclusive: Int, + toInclusive: Int, + allowOrdinal: Boolean = false + ): Int? { + return NumberExtractorUtils.extractOneIntegerInRange( + ts, fromInclusive, toInclusive + ) { NumberExtractorUtils.signBeforeNumber(ts) { numberExtractor.numberInteger(allowOrdinal) } } + } + + fun dateTime(): LocalDateTime? { + return ts.firstWhichUsesMostTokens({ dateTime(false) }, { dateTime(true) }) + } + + private fun dateTime(timeFirst: Boolean): LocalDateTime? { + var date: LocalDate? = null + var time: LocalTime? = null + + if (!timeFirst) { + date = relativeSpecialDay() + + if (date == null) { + val duration = Utils.firstNotNull( + this::relativeDuration, + dateTimeExtractor::relativeMonthDuration + ) + if (duration == null) { + date = date() + } else if (duration.nanos == 0L && duration.days != 0L) { + date = duration.applyAsOffsetToDateTime(now).toLocalDate() + } else if (duration.nanos != 0L && duration.days == 0L && duration.months == 0L && duration.years == 0L) { + time = duration.applyAsOffsetToDateTime(now).toLocalTime() + } else { + return duration.applyAsOffsetToDateTime(now) + } + } + } + + if (time == null) { + time = ts.tryOrSkipDateTimeIgnore(date != null) { this.timeWithAmpm() } + } + + if (date == null && time != null) { + val originalPosition = ts.position + val duration = ts.tryOrSkipDateTimeIgnore(true) { this.relativeDuration() } + if (duration == null) { + date = ts.tryOrSkipDateTimeIgnore( + true + ) { + Utils.firstNotNull(this::relativeSpecialDay, this::date) + } + } else if (duration.nanos == 0L && duration.days != 0L) { + date = duration.applyAsOffsetToDateTime(now).toLocalDate() + } else { + ts.position = originalPosition + } + } + + return if (date == null) { + time?.atDate(now.toLocalDate()) + } else { + if (time == null) date.atTime(now.toLocalTime()) else date.atTime(time) + } + } + + fun timeWithAmpm(): LocalTime? { + var time = time() + val pm: Boolean? + if (time == null) { + val momentOfDay = momentOfDay() ?: return null + + time = ts.tryOrSkipDateTimeIgnore(true) { this.time() } + if (time == null) { + return LocalTime.of(momentOfDay, 0) + } else { + pm = DateTimeExtractorUtils.isMomentOfDayPm(momentOfDay) + } + } else { + pm = ts.tryOrSkipDateTimeIgnore(true) { + Utils.firstNotNull( + dateTimeExtractor::ampm, + { momentOfDay()?.let(DateTimeExtractorUtils::isMomentOfDayPm) } + ) + } + } + + if (time.hour != 0 && pm != null) { + if (!pm && time.hour == 12) { + time = time.withHour(0) + } else if (pm && !DateTimeExtractorUtils.isMomentOfDayPm(time.hour)) { + time = time.withHour((time.hour + 12) % DateTimeExtractorUtils.HOURS_IN_DAY) + } + } + return time + } + + fun time(): LocalTime? { + val originalPosition = ts.position + val specialMinute = specialMinute() + + val hour = Utils.firstNotNull(this::noonMidnightLike, this::hour) + if (hour == null) { + ts.position = originalPosition + return null + } else if (specialMinute != null) { + return if (specialMinute < 0) { + LocalTime.of( + (hour + DateTimeExtractorUtils.HOURS_IN_DAY - 1) % DateTimeExtractorUtils.HOURS_IN_DAY, + 60 + specialMinute + ) // e.g. kvart i sex + } else { + LocalTime.of(hour, specialMinute) // e.g. halv åtta + } + } + var result = LocalTime.of(hour, 0) + + if (oClock()) { + return result // e.g. klockan tio + } + + val minute = ts.tryOrSkipDateTimeIgnore(true) { dateTimeExtractor.minute() } + if (minute == null) { + return result + } + result = result.withMinute(minute) + + val second = ts.tryOrSkipDateTimeIgnore(true) { dateTimeExtractor.second() } + if (second == null) { + return result + } + return result.withSecond(second) + } + + fun date(): LocalDate? { + var result = now.toLocalDate() + + val dayOfWeek = dateTimeExtractor.dayOfWeek() + val firstNum = ts.tryOrSkipDateTimeIgnore( + dayOfWeek != null + ) { extractIntegerInRange(1, 31, true) } + + if (firstNum == null && dayOfWeek != null) { + return result.plus((dayOfWeek - result.dayOfWeek.ordinal).toLong(), ChronoUnit.DAYS) + } + + val monthName = ts.tryOrSkipDateTimeIgnore( + firstNum != null + ) { dateTimeExtractor.monthName() } + if (monthName == null) { + result = if (firstNum == null) { + result.withDayOfMonth(1).withMonth(1) + } else { + val secondNumMax = if (firstNum <= 12) 31 else 12 + val secondNum = ts.tryOrSkipDateTimeIgnore( + true + ) { extractIntegerInRange(1, secondNumMax, true) } + + if (secondNum == null) { + return if (preferMonthBeforeDay && firstNum <= 12) { + result.withDayOfMonth(1).withMonth(firstNum) + } else { + result.withDayOfMonth(firstNum) + } + } else { + if ((preferMonthBeforeDay || secondNum > 12) && firstNum <= 12) { + result.withDayOfMonth(secondNum).withMonth(firstNum) + } else { + result.withDayOfMonth(firstNum).withMonth(secondNum) + } + } + } + } else { + result = result.withMonth(monthName) + + if (firstNum == null) { + val secondNum = ts.tryOrSkipDateTimeIgnore( + true + ) { extractIntegerInRange(1, 31, true) } + result = if (secondNum == null) { + result.withDayOfMonth(1) + } else { + result.withDayOfMonth(secondNum) + } + } else { + result = result.withDayOfMonth(firstNum) + } + } + val dayOrMonthFound = firstNum != null || monthName != null + + var bcad = ts.tryOrSkipDateTimeIgnore(dayOrMonthFound) { this.bcad() } + + val year = ts.tryOrSkipDateTimeIgnore( + dayOrMonthFound && bcad == null + ) { extractIntegerInRange(0, 999999999) } + if (year == null) { + if (dayOrMonthFound) { + return result + } + return null + } + + if (bcad == null) { + bcad = bcad() + } + return result.withYear(year * (if (bcad == null || bcad) 1 else -1)) + } + + + fun bcad(): Boolean? { + val bcad = dateTimeExtractor.bcad() + if (bcad != null && !bcad) { + // skip "era" in Swedish equivalents + val nextNotIgnore = ts.indexOfWithoutCategory("date_time_ignore", 0) + if (ts[nextNotIgnore].hasCategory("bcad_era")) { + ts.movePositionForwardBy(nextNotIgnore + 1) + } + } + return bcad + } + + fun noonMidnightLike(): Int? { + return noonMidnightLikeOrMomentOfDay("noon_midnight_like") + } + + fun momentOfDay(): Int? { + return noonMidnightLikeOrMomentOfDay("moment_of_day") + } + + private fun noonMidnightLikeOrMomentOfDay(category: String): Int? { + val originalPosition = ts.position + + var relativeIndicator = 0 + if (ts[0].hasCategory("pre_special_hour")) { + if (ts[0].hasCategory("pre_relative_indicator")) { + relativeIndicator = if (ts[0].hasCategory("negative")) -1 else 1 + ts.movePositionForwardBy(ts.indexOfWithoutCategory("date_time_ignore", 1)) + } else { + ts.movePositionForwardBy(1) + } + } + + if (ts[0].hasCategory(category)) { + ts.movePositionForwardBy(1) + return ((ts[-1].number!!.integerValue() + .toInt() + DateTimeExtractorUtils.HOURS_IN_DAY + relativeIndicator) + % DateTimeExtractorUtils.HOURS_IN_DAY) + } + + ts.position = originalPosition + return null + } + + fun hour(): Int? { + val originalPosition = ts.position + + ts.movePositionForwardBy(ts.indexOfWithoutCategory("pre_hour", 0)) + + val number = extractIntegerInRange(0, DateTimeExtractorUtils.HOURS_IN_DAY) + if (number == null) { + ts.position = originalPosition + return null + } + + return number % DateTimeExtractorUtils.HOURS_IN_DAY + } + + fun specialMinute(): Int? { + val originalPosition = ts.position + + ts.movePositionForwardBy(ts.indexOfWithoutCategory("pre_hour", 0)) + + val number = numberExtractor.numberNoOrdinal() + if (number != null) { + val minutes: Int + if (number.isDecimal && number.decimalValue() > 0.0 && number.decimalValue() < 1.0) { + minutes = Utils.roundToInt(number.decimalValue() * 60) + } else if (number.isInteger && number.integerValue() > 1 && number.integerValue() < 60) { + minutes = number.integerValue().toInt() + } else { + ts.position = originalPosition + return null + } + + val result = ts.tryOrSkipDateTimeIgnore(true) { + if (ts[0].hasCategory("special_minute_after")) { + // e.g. halv tolv (Swedish uses "half" differently - halv tolv = 11:30) + ts.movePositionForwardBy(1) + return@tryOrSkipDateTimeIgnore minutes + } else if (ts[0].hasCategory("special_minute_before")) { + // e.g. kvart i elva + ts.movePositionForwardBy(1) + return@tryOrSkipDateTimeIgnore -minutes + } else { + return@tryOrSkipDateTimeIgnore null + } + } + if (result != null) { + return result + } + } + + ts.position = originalPosition + return null + } + + fun oClock(): Boolean { + if (ts[0].hasCategory("pre_oclock")) { + val nextNotIgnore = ts.indexOfWithoutCategory("date_time_ignore", 1) + if (ts[nextNotIgnore].hasCategory("post_oclock")) { + ts.movePositionForwardBy(nextNotIgnore + 1) + return true + } + } else if (ts[0].hasCategory("oclock_combined")) { + ts.movePositionForwardBy(1) + return true + } + return false + } + + + private fun relativeSpecialDay(): LocalDate? { + val days = Utils.firstNotNull( + this::relativeYesterday, + dateTimeExtractor::relativeToday, + this::relativeTomorrow, + dateTimeExtractor::relativeDayOfWeekDuration + ) + if (days == null) { + return null + } + return now.toLocalDate().plusDays(days.toLong()) + } + + fun relativeYesterday(): Int? { + if (ts[0].hasCategory("day_adder_the") + && ts[1].hasCategory("day_adder_day") + && ts[2].hasCategory("day_adder_before") + && ts[3].hasCategory("yesterday") + ) { + ts.movePositionForwardBy(4) + return -2 // e.g. dagen före igår + } + + if (ts[0].hasCategory("day_adder_day") + && ts[1].hasCategory("day_adder_before") + && ts[2].hasCategory("yesterday") + ) { + ts.movePositionForwardBy(3) + return -2 // e.g. dagen före igår + } + + if (ts[0].hasCategory("yesterday")) { + ts.movePositionForwardBy(1) + return -1 // e.g. igår + } else { + return null + } + } + + fun relativeTomorrow(): Int? { + if (ts[0].hasCategory("day_adder_the") + && ts[1].hasCategory("day_adder_day") + && ts[2].hasCategory("day_adder_after") + && ts[3].hasCategory("tomorrow") + ) { + ts.movePositionForwardBy(4) + return 2 // e.g. dagen efter imorgon + } + + if (ts[0].hasCategory("day_adder_day") + && ts[1].hasCategory("day_adder_after") + && ts[2].hasCategory("tomorrow") + ) { + ts.movePositionForwardBy(3) + return 2 // e.g. dagen efter imorgon + } + + if (ts[0].hasCategory("tomorrow")) { + ts.movePositionForwardBy(1) + return 1 // e.g. imorgon + } else { + return null + } + } + + fun relativeDuration(): Duration? { + return dateTimeExtractor.relativeIndicatorDuration( + { durationExtractor.duration() }, + { duration -> duration.multiply(-1) } + ) + } +} diff --git a/numbers/src/main/java/org/dicio/numbers/lang/sv/SwedishFormatter.kt b/numbers/src/main/java/org/dicio/numbers/lang/sv/SwedishFormatter.kt new file mode 100644 index 00000000..ad8d353c --- /dev/null +++ b/numbers/src/main/java/org/dicio/numbers/lang/sv/SwedishFormatter.kt @@ -0,0 +1,357 @@ +package org.dicio.numbers.lang.sv + +import org.dicio.numbers.formatter.Formatter +import org.dicio.numbers.unit.MixedFraction +import org.dicio.numbers.util.Utils +import java.time.LocalTime +import java.time.format.DateTimeFormatter +import java.util.Locale +import kotlin.math.abs + +class SwedishFormatter : Formatter("config/sv-se") { + + override fun niceNumber(mixedFraction: MixedFraction, speech: Boolean): String { + if (speech) { + val sign = if (mixedFraction.negative) "minus " else "" + if (mixedFraction.numerator == 0) { + return sign + pronounceNumber(mixedFraction.whole.toDouble(), 0, true, false, false) + } + + var denominatorString = when (mixedFraction.denominator) { + 2 -> "halv" + 4 -> "kvarts" + else -> { + // use ordinal: only halv and kvarts are exceptions + pronounceNumber(mixedFraction.denominator.toDouble(), 0, true, false, true) + } + } + + val numeratorString: String + if (mixedFraction.numerator == 1) { + numeratorString = "en" + } else { + numeratorString = + pronounceNumber(mixedFraction.numerator.toDouble(), 0, true, false, false) + denominatorString += "ar" + } + + return if (mixedFraction.whole == 0L) { + "$sign$numeratorString $denominatorString" + } else { + (sign + pronounceNumber(mixedFraction.whole.toDouble(), 0, true, false, false) + + " och " + numeratorString + " " + denominatorString) + } + } else { + return niceNumberNotSpeech(mixedFraction) + } + } + + override fun pronounceNumber( + number: Double, + places: Int, + shortScale: Boolean, + scientific: Boolean, + ordinal: Boolean + ): String { + if (number == Double.POSITIVE_INFINITY) { + return "oändlighet" + } else if (number == Double.NEGATIVE_INFINITY) { + return "negativ oändlighet" + } else if (java.lang.Double.isNaN(number)) { + return "inte ett tal" + } + + // also using scientific mode if the number is too big to be spoken fully + if (scientific || abs(number) > 999999999999999934463.0) { + val scientificFormatted = String.format(Locale.ENGLISH, "%E", number) + val parts = scientificFormatted.split("E".toRegex(), limit = 2).toTypedArray() + val power = parts[1].toInt().toDouble() + + if (power != 0.0) { + val n = parts[0].toDouble() + return String.format( + "%s%s gånger tio upphöjt till %s%s", + if (n < 0) "negativ " else "", + pronounceNumber(abs(n), places, true, false, false), + if (power < 0) "negativ " else "", + pronounceNumber(abs(power), places, true, false, false) + ) + } + } + + val result = StringBuilder() + var varNumber = number + if (varNumber < 0) { + varNumber = -varNumber + if (places != 0 || varNumber >= 0.5) { + result.append(if (scientific) "negativ " else "minus ") + } + } + + val realPlaces = Utils.decimalPlacesNoFinalZeros(varNumber, places) + val numberIsWhole = realPlaces == 0 + val numberLong = varNumber.toLong() + (if (varNumber % 1 >= 0.5 && numberIsWhole) 1 else 0) + + // Sweden always uses long scale (miljard, biljon) + if (!ordinal && NUMBER_NAMES.containsKey(numberLong)) { + if (varNumber > 90) { + result.append("ett ") + } + result.append(NUMBER_NAMES[numberLong]) + } else { + // Sweden uses long scale + var ordi = ordinal && numberIsWhole + val groups = Utils.splitByModulus(numberLong, 1000000) + val groupNames = ArrayList() + for (i in groups.indices) { + val z = groups[i] + if (z == 0L) { + continue // skip 000000 groups + } + + var groupName: String + if (z < 1000) { + groupName = subThousand(z, i == 0 && ordi) + } else { + groupName = subThousand(z / 1000, false) + "tusen" + if (z % 1000 != 0L) { + groupName += subThousand(z % 1000, i == 0 && ordi) + } else if (i == 0 && ordi) { + if (z / 1000 == 1L) { + groupName = "tusende" // remove "ett" from "ett tusende" + } else { + groupName += "de" + } + } + } + + if (i != 0) { + val magnitude = Utils.longPow(1000000, i) + if (ordi) { + if (z == 1L) { + groupName = ORDINAL_NAMES_LONG_SCALE[magnitude]!! + } else { + groupName += ORDINAL_NAMES_LONG_SCALE[magnitude] + } + } else { + groupName += NUMBER_NAMES_LONG_SCALE[magnitude] + } + } + + groupNames.add(groupName) + ordi = false + } + + appendSplitGroups(result, groupNames) + } + + if (realPlaces > 0) { + if (varNumber < 1.0 && (result.isEmpty() || "minus ".contentEquals(result))) { + result.append("noll") + } + result.append(" komma") + + val fractionalPart = String.format("%." + realPlaces + "f", varNumber % 1) + for (i in 2 until fractionalPart.length) { + result.append(" ") + result.append(NUMBER_NAMES[(fractionalPart[i].code - '0'.code).toLong()]) + } + } + + return result.toString() + } + + override fun niceTime( + time: LocalTime, + speech: Boolean, + use24Hour: Boolean, + showAmPm: Boolean + ): String { + if (speech) { + if (use24Hour) { + val result = StringBuilder() + result.append("klockan ") + result.append(pronounceNumberDuration(time.hour.toLong())) + + if (time.minute == 0) { + // "klockan tretton" (nothing more) + } else { + result.append(" ") + if (time.minute < 10) { + result.append("noll ") + } + result.append(pronounceNumberDuration(time.minute.toLong())) + } + + return result.toString() + } else { + if (time.hour == 0 && time.minute == 0) { + return "midnatt" + } else if (time.hour == 12 && time.minute == 0) { + return "middag" + } + + val normalizedHour = (time.hour + 11) % 12 + 1 + val result = StringBuilder() + if (time.minute == 15) { + result.append("kvart över ") + result.append(pronounceNumberDuration(normalizedHour.toLong())) + } else if (time.minute == 30) { + result.append("halv ") + result.append(pronounceNumberDuration((normalizedHour % 12 + 1).toLong())) + } else if (time.minute == 45) { + result.append("kvart i ") + result.append(pronounceNumberDuration((normalizedHour % 12 + 1).toLong())) + } else { + result.append(pronounceNumberDuration(normalizedHour.toLong())) + + if (time.minute == 0) { + // nothing more to add + } else { + result.append(" ") + if (time.minute < 10) { + result.append("noll ") + } + result.append(pronounceNumberDuration(time.minute.toLong())) + } + } + + if (showAmPm) { + result.append(if (time.hour >= 12) " em" else " fm") + } + return result.toString() + } + } else { + if (use24Hour) { + return time.format(DateTimeFormatter.ofPattern("HH:mm", Locale("sv", "SE"))) + } else { + val result = time.format( + DateTimeFormatter.ofPattern( + if (showAmPm) "K:mm a" else "K:mm", Locale("sv", "SE") + ) + ) + return if (result.startsWith("0:")) { + "12:" + result.substring(2) + } else { + result + } + } + } + } + + + /** + * @param n must be 0 <= n <= 999 + * @param ordinal whether to return an ordinal number + * @return the string representation of a number smaller than 1000 + */ + private fun subThousand(n: Long, ordinal: Boolean): String { + if (ordinal && ORDINAL_NAMES.containsKey(n)) { + return ORDINAL_NAMES[n]!! + } else if (n < 100) { + if (!ordinal && NUMBER_NAMES.containsKey(n)) { + return NUMBER_NAMES[n]!! + } + + // n is surely => 20 from here on, since all n < 20 are in the maps + return (NUMBER_NAMES[n - n % 10] + + (if (n % 10 > 0) subThousand(n % 10, ordinal) else "")) + } else { + val hundredPart = if (n / 100 == 1L) "etthundra" else NUMBER_NAMES[n / 100] + "hundra" + return (hundredPart + + (if (n % 100 > 0) subThousand(n % 100, ordinal) + else (if (ordinal) "de" else ""))) + } + } + + /** + * @param result the StringBuilder to append the group names to + * @param groupNames the group names + */ + private fun appendSplitGroups(result: StringBuilder, groupNames: List) { + // Swedish does not use commas between groups like English + for (i in groupNames.size - 1 downTo 0) { + result.append(groupNames[i]) + } + } + + companion object { + val NUMBER_NAMES = mapOf( + 0L to "noll", + 1L to "ett", + 2L to "två", + 3L to "tre", + 4L to "fyra", + 5L to "fem", + 6L to "sex", + 7L to "sju", + 8L to "åtta", + 9L to "nio", + 10L to "tio", + 11L to "elva", + 12L to "tolv", + 13L to "tretton", + 14L to "fjorton", + 15L to "femton", + 16L to "sexton", + 17L to "sjutton", + 18L to "arton", + 19L to "nitton", + 20L to "tjugo", + 30L to "trettio", + 40L to "fyrtio", + 50L to "femtio", + 60L to "sextio", + 70L to "sjuttio", + 80L to "åttio", + 90L to "nittio", + 100L to "hundra", + 1000L to "tusen", + 1000000L to "miljon", + ) + + // Sverige använder long scale + val NUMBER_NAMES_LONG_SCALE = NUMBER_NAMES + mapOf( + 1000000000000L to "biljon", + 1000000000000000000L to "triljon", + ) + + val ORDINAL_NAMES = mapOf( + 1L to "första", + 2L to "andra", + 3L to "tredje", + 4L to "fjärde", + 5L to "femte", + 6L to "sjätte", + 7L to "sjunde", + 8L to "åttonde", + 9L to "nionde", + 10L to "tionde", + 11L to "elfte", + 12L to "tolfte", + 13L to "trettonde", + 14L to "fjortonde", + 15L to "femtonde", + 16L to "sextonde", + 17L to "sjuttonde", + 18L to "artonde", + 19L to "nittonde", + 20L to "tjugonde", + 30L to "trettionde", + 40L to "fyrtionde", + 50L to "femtionde", + 60L to "sextionde", + 70L to "sjuttionde", + 80L to "åttionde", + 90L to "nittionde", + 100L to "hundrade", + 1000L to "tusende", + 1000000L to "miljonte", + ) + + val ORDINAL_NAMES_LONG_SCALE = ORDINAL_NAMES + mapOf( + 1000000000000L to "biljonte", + 1000000000000000000L to "triljonte", + ) + } +} diff --git a/numbers/src/main/java/org/dicio/numbers/lang/sv/SwedishNumberExtractor.kt b/numbers/src/main/java/org/dicio/numbers/lang/sv/SwedishNumberExtractor.kt new file mode 100644 index 00000000..348cf376 --- /dev/null +++ b/numbers/src/main/java/org/dicio/numbers/lang/sv/SwedishNumberExtractor.kt @@ -0,0 +1,311 @@ +package org.dicio.numbers.lang.sv + +import org.dicio.numbers.parser.lexer.TokenStream +import org.dicio.numbers.unit.Number +import org.dicio.numbers.util.NumberExtractorUtils + +class SwedishNumberExtractor internal constructor( + private val ts: TokenStream, + private val shortScale: Boolean +) { + fun numberPreferOrdinal(): Number? { + var number = numberSuffixMultiplier() + if (number == null) { + number = numberSignPoint(true) + } + + return divideByDenominatorIfPossible(number) + } + + fun numberPreferFraction(): Number? { + var number = numberSuffixMultiplier() + if (number == null) { + number = numberSignPoint(false) + } + + number = divideByDenominatorIfPossible(number) + + if (number == null) { + number = numberSignPoint(true) + } + return number + } + + fun numberNoOrdinal(): Number? { + var number = numberSuffixMultiplier() + if (number == null) { + number = numberSignPoint(false) + } + + number = divideByDenominatorIfPossible(number) + + return number + } + + + fun divideByDenominatorIfPossible(numberToEdit: Number?): Number? { + if (numberToEdit == null) { + if (ts[0].isValue("en") || ts[0].isValue("ett")) { + val originalPosition = ts.position + ts.movePositionForwardBy(1) + + val denominator = numberInteger(true) + if (denominator != null && denominator.isOrdinal && denominator.moreThan(2)) { + return Number(1).divide(denominator) + } else { + ts.position = originalPosition + } + } + return null + } + + if (!numberToEdit.isOrdinal && !numberToEdit.isDecimal + && !ts[0].hasCategory("ignore") + ) { + val originalPosition = ts.position + val denominator = numberInteger(true) + if (denominator == null) { + if (ts[0].hasCategory("suffix_multiplier")) { + ts.movePositionForwardBy(1) + + val multiplier = ts[-1].number + if (multiplier!!.isDecimal && (1 / multiplier.decimalValue()).toLong() + .toDouble() + == (1 / multiplier.decimalValue()) + ) { + return numberToEdit.divide((1 / multiplier.decimalValue()).toLong()) + } + + return numberToEdit.multiply(multiplier) + } + } else if (denominator.isOrdinal && denominator.moreThan(2)) { + return numberToEdit.divide(denominator) + } else { + ts.position = originalPosition + } + } + return numberToEdit + } + + fun numberSuffixMultiplier(): Number? { + if (ts[0].hasCategory("suffix_multiplier")) { + ts.movePositionForwardBy(1) + return ts[-1].number + } else if ((ts[0].isValue("en") || ts[0].isValue("ett")) && ts[1].hasCategory("suffix_multiplier")) { + ts.movePositionForwardBy(2) + return ts[-1].number + } else { + return null + } + } + + fun numberSignPoint(allowOrdinal: Boolean): Number? { + return NumberExtractorUtils.signBeforeNumber(ts) { numberPoint(allowOrdinal) } + } + + fun numberPoint(allowOrdinal: Boolean): Number? { + var n = numberInteger(allowOrdinal) + if (n != null && n.isOrdinal) { + return n + } + + if (ts[0].hasCategory("point")) { + if (!ts[1].hasCategory("digit_after_point") + && (!NumberExtractorUtils.isRawNumber(ts[1]) || ts[2].hasCategory("ordinal_suffix")) + ) { + return n + } + + ts.movePositionForwardBy(1) + if (n == null) { + n = Number(0.0) + } + + var magnitude = 0.1 + if (ts[0].value.length > 1 && NumberExtractorUtils.isRawNumber(ts[0])) { + for (i in 0 until ts[0].value.length) { + n = n!!.plus((ts[0].value[i].code - '0'.code) * magnitude) + magnitude /= 10.0 + } + ts.movePositionForwardBy(1) + } else { + while (true) { + if (ts[0].hasCategory("digit_after_point") + || (ts[0].value.length == 1 && NumberExtractorUtils.isRawNumber( + ts[0] + ) + && !ts[1].hasCategory("ordinal_suffix")) + ) { + n = n!!.plus(ts[0].number!!.multiply(magnitude)) + magnitude /= 10.0 + } else { + break + } + ts.movePositionForwardBy(1) + } + } + } else if (n != null && ts[0].hasCategory("fraction_separator")) { + val originalPosition = ts.position + ts.movePositionForwardBy(1) + if (ts[0].hasCategory("fraction_separator_secondary")) { + ts.movePositionForwardBy(1) + } + + val denominator = numberInteger(false) + if (denominator == null || (denominator.isInteger && denominator.integerValue() == 0L) + || (denominator.isDecimal && denominator.decimalValue() == 0.0) + ) { + ts.position = originalPosition + } else { + return n.divide(denominator) + } + } + + return n + } + + fun numberInteger(allowOrdinal: Boolean): Number? { + if (ts[0].hasCategory("ignore") + && (!ts[0].isValue("en") && !ts[0].isValue("ett") || ts[1].hasCategory("ignore")) + ) { + return null + } + + // Sweden uses long scale + var n = NumberExtractorUtils.numberMadeOfGroups( + ts, + allowOrdinal, + ::numberGroupLongScale + ) + if (n == null) { + return NumberExtractorUtils.numberBigRaw(ts, allowOrdinal) + } else if (n.isOrdinal) { + return n + } + + if (n.lessThan(100)) { + val nextNotIgnore = ts.indexOfWithoutCategory("ignore", 0) + if (ts[nextNotIgnore].hasCategory("hundred")) { + val ordinal = ts[nextNotIgnore].hasCategory("ordinal") + if (allowOrdinal || !ordinal) { + ts.movePositionForwardBy(nextNotIgnore + 1) + return n.multiply(100).withOrdinal(ordinal) + } + } + } + + if (n.lessThan(1000)) { + if (NumberExtractorUtils.isRawNumber(ts[-1]) && ts[0].hasCategory("thousand_separator") && + ts[1].value.length == 3 && NumberExtractorUtils.isRawNumber(ts[1]) + ) { + val originalPosition = ts.position - 1 + + while (ts[0].hasCategory("thousand_separator") && ts[1].value.length == 3 && + NumberExtractorUtils.isRawNumber(ts[1]) + ) { + n = n!!.multiply(1000).plus(ts[1].number) + ts.movePositionForwardBy(2) + } + + if (ts[0].hasCategory("ordinal_suffix")) { + if (allowOrdinal) { + ts.movePositionForwardBy(1) + return n!!.withOrdinal(true) + } else { + ts.position = originalPosition + return null + } + } + } + } + + return n + } + + companion object { + @JvmStatic + fun numberGroupLongScale( + ts: TokenStream, + allowOrdinal: Boolean, + lastMultiplier: Double + ): Number? { + if (lastMultiplier < 1000000) { + return null + } + + val originalPosition = ts.position + var first = NumberExtractorUtils.numberGroupShortScale(ts, allowOrdinal, 1000000.0) + if (first == null) { + first = NumberExtractorUtils.numberLessThan1000(ts, allowOrdinal) + if (first != null && first.isOrdinal) { + return first + } + + if (first == null) { + val nextNotIgnore = ts.indexOfWithoutCategory("ignore", 0) + if (NumberExtractorUtils.isRawNumber(ts[nextNotIgnore]) + && ts[nextNotIgnore].number!!.lessThan(1000000) + ) { + val ordinal = ts[nextNotIgnore + 1].hasCategory("ordinal_suffix") + if (ordinal) { + if (!allowOrdinal) { + return null + } + ts.movePositionForwardBy(nextNotIgnore + 2) + return ts[-2].number!!.withOrdinal(true) + } + ts.movePositionForwardBy(nextNotIgnore + 1) + first = ts[-1].number + } + } + } else { + if (first.isOrdinal || first.lessThan(1000)) { + return first + } + + val second = NumberExtractorUtils.numberLessThan1000(ts, allowOrdinal) + if (second != null) { + first = first.plus(second) + if (second.isOrdinal) { + return first.withOrdinal(true) + } + } + } + + val nextNotIgnore = ts.indexOfWithoutCategory("ignore", 0) + val ordinal = ts[nextNotIgnore].hasCategory("ordinal") + if (ts[nextNotIgnore].hasCategory("multiplier") && (allowOrdinal || !ordinal) + && ts[nextNotIgnore].number!!.moreThan(1000) + ) { + val multiplier = shortMultiplierToLongScale(ts[nextNotIgnore].number) + if (multiplier!!.lessThan(lastMultiplier)) { + ts.movePositionForwardBy(nextNotIgnore + 1) + return if (first == null) { + multiplier.withOrdinal(ordinal) + } else { + multiplier.multiply(first).withOrdinal(ordinal) + } + } + } else { + return first + } + + ts.position = originalPosition + return null + } + + fun shortMultiplierToLongScale(shortScaleMultiplier: Number?): Number? { + return if (shortScaleMultiplier!!.integerValue() == 1000000000L) { + Number(1000000000000L) // miljard + } else if (shortScaleMultiplier.integerValue() == 1000000000000L) { + Number(1000000000000000000L) // biljon + } else if (shortScaleMultiplier.integerValue() == 1000000000000000L) { + Number(1e24) // biljard + } else if (shortScaleMultiplier.integerValue() == 1000000000000000000L) { + Number(1e30) // triljon + } else { + shortScaleMultiplier // miljon + } + } + } +} diff --git a/numbers/src/main/java/org/dicio/numbers/lang/sv/SwedishParser.kt b/numbers/src/main/java/org/dicio/numbers/lang/sv/SwedishParser.kt new file mode 100644 index 00000000..e86d7c23 --- /dev/null +++ b/numbers/src/main/java/org/dicio/numbers/lang/sv/SwedishParser.kt @@ -0,0 +1,40 @@ +package org.dicio.numbers.lang.sv + +import org.dicio.numbers.parser.Parser +import org.dicio.numbers.parser.lexer.TokenStream +import org.dicio.numbers.unit.Duration +import org.dicio.numbers.unit.Number +import org.dicio.numbers.util.DurationExtractorUtils +import java.time.LocalDateTime + +class SwedishParser : Parser("config/sv-se") { + override fun extractNumber( + tokenStream: TokenStream, + shortScale: Boolean, + preferOrdinal: Boolean + ): () -> Number? { + val numberExtractor = SwedishNumberExtractor(tokenStream, shortScale) + return if (preferOrdinal) { + numberExtractor::numberPreferOrdinal + } else { + numberExtractor::numberPreferFraction + } + } + + override fun extractDuration( + tokenStream: TokenStream, + shortScale: Boolean + ): () -> Duration? { + val numberExtractor = SwedishNumberExtractor(tokenStream, shortScale) + return DurationExtractorUtils(tokenStream, numberExtractor::numberNoOrdinal)::duration + } + + override fun extractDateTime( + tokenStream: TokenStream, + shortScale: Boolean, + preferMonthBeforeDay: Boolean, + now: LocalDateTime + ): () -> LocalDateTime? { + return SwedishDateTimeExtractor(tokenStream, shortScale, preferMonthBeforeDay, now)::dateTime + } +} diff --git a/numbers/src/main/resources/config/sv-se/date_time.json b/numbers/src/main/resources/config/sv-se/date_time.json new file mode 100644 index 00000000..de4bf1e0 --- /dev/null +++ b/numbers/src/main/resources/config/sv-se/date_time.json @@ -0,0 +1,129 @@ +{ + "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": "^\\d{3}$", "format": "{x_in_x00} hundra"}, + "default": "{number}" + }, + "thousand_format": { + "1": {"match": "^\\d00\\d$", "format": "{x_in_x000} tusen"}, + "2": {"match": "^1\\d00$", "format": "{xx_in_xx00} hundra"}, + "3": {"match": "^\\d{2}00$", "format": "{x0_in_x000} {x_in_x00} hundra"}, + "4": {"match": "^(1\\d{3})|(\\d0\\d{2})$", "format": "{xx_in_xx00}"}, + "5": {"match": "^\\d{4}$", "format": "{x0_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": "^\\d00\\d$", "format": "{formatted_thousand} {formatted_decade} {bc}"}, + "6": {"match": "^\\d{2}0\\d$", "format": "{formatted_thousand} noll {formatted_decade} {bc}"}, + "7": {"match": "^\\d{4}$", "format": "{formatted_thousand} {formatted_decade} {bc}"}, + "default": "{year} {bc}", + "bc": "före kristus" + }, + "date_format": { + "date_full": "{weekday}, den {day} {month}, {formatted_year}", + "date_full_no_year": "{weekday}, den {day} {month}", + "date_full_no_year_month": "{weekday}, den {day}", + "today": "idag", + "tomorrow": "imorgon", + "yesterday": "igår" + }, + "date_time_format": { + "date_time": "{formatted_date} klockan {formatted_time}" + }, + "weekday": { + "0": "måndag", + "1": "tisdag", + "2": "onsdag", + "3": "torsdag", + "4": "fredag", + "5": "lördag", + "6": "söndag" + }, + "date": { + "1": "första", + "2": "andra", + "3": "tredje", + "4": "fjärde", + "5": "femte", + "6": "sjätte", + "7": "sjunde", + "8": "åttonde", + "9": "nionde", + "10": "tionde", + "11": "elfte", + "12": "tolfte", + "13": "trettonde", + "14": "fjortonde", + "15": "femtonde", + "16": "sextonde", + "17": "sjuttonde", + "18": "artonde", + "19": "nittonde", + "20": "tjugonde", + "21": "tjugoförsta", + "22": "tjugoandra", + "23": "tjugotredje", + "24": "tjugofjärde", + "25": "tjugofemte", + "26": "tjugosjätte", + "27": "tjugosjunde", + "28": "tjugoåttonde", + "29": "tjugonionde", + "30": "trettionde", + "31": "trettiförsta" + }, + "month": { + "1": "januari", + "2": "februari", + "3": "mars", + "4": "april", + "5": "maj", + "6": "juni", + "7": "juli", + "8": "augusti", + "9": "september", + "10": "oktober", + "11": "november", + "12": "december" + }, + "number": { + "0": "noll", + "1": "ett", + "2": "två", + "3": "tre", + "4": "fyra", + "5": "fem", + "6": "sex", + "7": "sju", + "8": "åtta", + "9": "nio", + "10": "tio", + "11": "elva", + "12": "tolv", + "13": "tretton", + "14": "fjorton", + "15": "femton", + "16": "sexton", + "17": "sjutton", + "18": "arton", + "19": "nitton", + "20": "tjugo", + "30": "trettio", + "40": "förtio", + "50": "femtio", + "60": "sextio", + "70": "sjuttio", + "80": "åttio", + "90": "nittio" + } +} \ No newline at end of file diff --git a/numbers/src/main/resources/config/sv-se/day.word b/numbers/src/main/resources/config/sv-se/day.word new file mode 100644 index 00000000..73e686aa --- /dev/null +++ b/numbers/src/main/resources/config/sv-se/day.word @@ -0,0 +1 @@ +dag \ No newline at end of file diff --git a/numbers/src/main/resources/config/sv-se/days.word b/numbers/src/main/resources/config/sv-se/days.word new file mode 100644 index 00000000..594c6084 --- /dev/null +++ b/numbers/src/main/resources/config/sv-se/days.word @@ -0,0 +1 @@ +dagar \ No newline at end of file diff --git a/numbers/src/main/resources/config/sv-se/hour.word b/numbers/src/main/resources/config/sv-se/hour.word new file mode 100644 index 00000000..4f92b0c1 --- /dev/null +++ b/numbers/src/main/resources/config/sv-se/hour.word @@ -0,0 +1 @@ +timme \ No newline at end of file diff --git a/numbers/src/main/resources/config/sv-se/hours.word b/numbers/src/main/resources/config/sv-se/hours.word new file mode 100644 index 00000000..d97697a0 --- /dev/null +++ b/numbers/src/main/resources/config/sv-se/hours.word @@ -0,0 +1 @@ +timmar \ No newline at end of file diff --git a/numbers/src/main/resources/config/sv-se/minute.word b/numbers/src/main/resources/config/sv-se/minute.word new file mode 100644 index 00000000..4b98366b --- /dev/null +++ b/numbers/src/main/resources/config/sv-se/minute.word @@ -0,0 +1 @@ +minut \ No newline at end of file diff --git a/numbers/src/main/resources/config/sv-se/minutes.word b/numbers/src/main/resources/config/sv-se/minutes.word new file mode 100644 index 00000000..caf1b024 --- /dev/null +++ b/numbers/src/main/resources/config/sv-se/minutes.word @@ -0,0 +1 @@ +minuter \ No newline at end of file diff --git a/numbers/src/main/resources/config/sv-se/second.word b/numbers/src/main/resources/config/sv-se/second.word new file mode 100644 index 00000000..300f8e50 --- /dev/null +++ b/numbers/src/main/resources/config/sv-se/second.word @@ -0,0 +1 @@ +sekund \ No newline at end of file diff --git a/numbers/src/main/resources/config/sv-se/seconds.word b/numbers/src/main/resources/config/sv-se/seconds.word new file mode 100644 index 00000000..aa5fc120 --- /dev/null +++ b/numbers/src/main/resources/config/sv-se/seconds.word @@ -0,0 +1 @@ +sekunder \ No newline at end of file diff --git a/numbers/src/main/resources/config/sv-se/tokenizer.json b/numbers/src/main/resources/config/sv-se/tokenizer.json new file mode 100644 index 00000000..570b2938 --- /dev/null +++ b/numbers/src/main/resources/config/sv-se/tokenizer.json @@ -0,0 +1,788 @@ +{ + "spaces": " \t\n\f\r:;_!?<>|=()[]{}»«*~^`'\"", + "characters_as_word": "%‰#-+.,/", + "raw_number_categories": [ + "number", + "raw" + ], + "plural_endings": [ + "r", + "n", + "or", + "ar", + "er" + ], + "word_matches": [ + { + "categories": [ + "ignore", + "date_time_ignore" + ], + "values": [ + "och" + ] + }, + { + "categories": [ + "ignore", + "date_time_ignore", + "day_adder_the", + "bcad_after" + ], + "values": [ + "en", + "ett" + ] + }, + { + "categories": [ + "ignore", + "date_time_ignore", + "day_adder_the" + ], + "values": [ + "en", + "ett" + ] + }, + { + "categories": [ + "ignore", + "date_time_ignore", + "thousand_separator" + ], + "values": [ + " ", + "." + ] + }, + { + "categories": [ + "ordinal_suffix" + ], + "values": [ + ":a", + ":e" + ] + }, + { + "categories": [ + "point" + ], + "values": [ + "punkt" + ] + }, + { + "categories": [ + "point", + "post_oclock" + ], + "values": [ + "prick" + ] + }, + { + "categories": [ + "point", + "ignore", + "date_time_ignore" + ], + "values": [ + ".", + "," + ] + }, + { + "categories": [ + "fraction_separator" + ], + "values": [ + "över", + "delat" + ] + }, + { + "categories": [ + "fraction_separator", + "date_time_ignore" + ], + "values": [ + "/" + ] + }, + { + "categories": [ + "fraction_separator_secondary" + ], + "values": [ + "med" + ] + }, + { + "categories": [ + "sign", + "positive" + ], + "values": [ + "plus", + "+" + ] + }, + { + "categories": [ + "sign", + "negative" + ], + "values": [ + "minus" + ] + }, + { + "categories": [ + "ignore", + "date_time_ignore", + "sign", + "negative" + ], + "values": [ + "-" + ] + }, + { + "categories": [ + "duration_separator", + "date_time_ignore" + ], + "values": [ + "av" + ] + }, + { + "categories": [ + "yesterday" + ], + "values": [ + "igår" + ] + }, + { + "categories": [ + "today" + ], + "values": [ + "idag" + ] + }, + { + "categories": [ + "tomorrow" + ], + "values": [ + "imorgon" + ] + }, + { + "categories": [ + "day_adder_the", + "date_time_ignore", + "pre_hour", + "pre_special_hour" + ], + "values": [ + "den" + ] + }, + { + "categories": [ + "day_adder_day" + ], + "values": [ + "dag" + ] + }, + { + "categories": [ + "pre_relative_indicator", + "post_relative_indicator", + "positive", + "day_adder_after", + "special_minute_after", + "pre_special_hour" + ], + "values": [ + "efter" + ] + }, + { + "categories": [ + "day_adder_before", + "special_minute_before", + "bcad_before", + "pre_relative_indicator", + "post_relative_indicator", + "negative", + "pre_special_hour" + ], + "values": [ + "före" + ] + }, + { + "categories": [ + "date_time_ignore", + "special_minute_before" + ], + "values": [ + "till" + ] + }, + { + "categories": [ + "special_minute_after", + "pre_relative_indicator", + "negative" + ], + "values": [ + "över", + "efter" + ] + }, + { + "categories": [ + "pre_hour" + ], + "values": [ + "timme", + "timmar" + ] + }, + { + "categories": [ + "pre_hour", + "pre_special_hour" + ], + "values": [ + "kl", + "klockan" + ] + }, + { + "categories": [ + "pre_special_hour" + ], + "values": [ + "denna", + "detta", + "dessa" + ] + }, + { + "categories": [ + "pre_special_hour", + "pre_relative_indicator", + "positive", + "pre_oclock" + ], + "values": [ + "på" + ] + }, + { + "categories": [ + "pre_relative_indicator", + "positive" + ], + "values": [ + "nästa", + "kommande" + ] + }, + { + "categories": [ + "date_time_ignore", + "pre_relative_indicator", + "positive" + ], + "values": [ + "om" + ] + }, + { + "categories": [ + "pre_relative_indicator", + "post_relative_indicator", + "positive" + ], + "values": [ + "följande" + ] + }, + { + "categories": [ + "post_relative_indicator", + "negative" + ], + "values": [ + "sedan" + ] + }, + { + "categories": [ + "pre_relative_indicator", + "negative" + ], + "values": [ + "förra", + "föregående" + ] + }, + { + "categories": [ + "bcad_before" + ], + "values": [ + "f.kr" + ] + }, + { + "categories": [ + "bcad_after" + ], + "values": [ + "e.kr" + ] + }, + { + "categories": [ + "bcad_identifier" + ], + "values": [ + "kristus", + "domini", + "d" + ] + }, + { + "categories": [ + "bcad_identifier", + "bcad_after" + ], + "values": [ + "c", + "gemensam", + "nuvarande" + ] + }, + { + "categories": [ + "bcad_before_combined" + ], + "values": [ + "bc", + "bce" + ] + }, + { + "categories": [ + "bcad_after_combined" + ], + "values": [ + "ad", + "ce" + ] + }, + { + "categories": [ + "bcad_identifier", + "bcad_era" + ], + "values": [ + "era", + "e" + ] + }, + { + "categories": [ + "post_oclock" + ], + "values": [ + "prick" + ] + }, + { + "categories": [ + "oclock_combined" + ], + "values": [ + "exakt", + "prick" + ] + } + ], + "number_mappings": [ + { + "categories": [ + "number", + "digit", + "digit_after_point" + ], + "values": { + "noll": 0, + "ett": 1, + "en": 1, + "två": 2, + "tre": 3, + "fyra": 4, + "fem": 5, + "sex": 6, + "sju": 7, + "åtta": 8, + "nio": 9 + } + }, + { + "categories": [ + "number", + "teen" + ], + "values": { + "tio": 10, + "elva": 11, + "tolv": 12, + "tretton": 13, + "fjorton": 14, + "femton": 15, + "sexton": 16, + "sjutton": 17, + "arton": 18, + "nitton": 19 + } + }, + { + "categories": [ + "number", + "tens" + ], + "values": { + "tjugo": 20, + "trettio": 30, + "fyrtio": 40, + "femtio": 50, + "sextio": 60, + "sjuttio": 70, + "åttio": 80, + "nittio": 90 + } + }, + { + "categories": [ + "number", + "hundred" + ], + "values": { + "hundra": 100 + } + }, + { + "categories": [ + "number", + "multiplier" + ], + "values": { + "tusen": 1000, + "miljon": 1000000, + "miljoner": 1000000, + "miljard": 1000000000, + "miljarder": 1000000000, + "biljon": 1000000000000, + "biljoner": 1000000000000, + "biljard": 1000000000000000, + "biljarder": 1000000000000000 + } + }, + { + "categories": [ + "number", + "ordinal", + "digit" + ], + "values": { + "första": 1, + "andra": 2, + "tredje": 3, + "fjärde": 4, + "femte": 5, + "sjätte": 6, + "sjunde": 7, + "åttonde": 8, + "nionde": 9 + } + }, + { + "categories": [ + "number", + "ordinal", + "teen" + ], + "values": { + "tionde": 10, + "elfte": 11, + "tolfte": 12, + "trettonde": 13, + "fjortonde": 14, + "femtonde": 15, + "sextonde": 16, + "sjuttonde": 17, + "artonde": 18, + "nittonde": 19 + } + }, + { + "categories": [ + "number", + "ordinal", + "tens" + ], + "values": { + "tjugonde": 20, + "trettionde": 30, + "fyrtionde": 40, + "femtionde": 50, + "sextionde": 60, + "sjuttionde": 70, + "åttionde": 80, + "nittionde": 90 + } + }, + { + "categories": [ + "number", + "ordinal", + "hundred" + ], + "values": { + "hundrade": 100 + } + }, + { + "categories": [ + "number", + "ordinal", + "multiplier" + ], + "values": { + "tusende": 1000, + "miljonte": 1000000, + "miljardte": 1000000000, + "biljonte": 1000000000000, + "biljardte": 1000000000000000 + } + }, + { + "categories": [ + "number", + "suffix_multiplier" + ], + "values": { + "halv": 0.5, + "hälft": 0.5, + "kvart": 0.25, + "par": 2, + "dussin": 12, + "gros": 144, + "tjog": 20, + "procent": 0.01, + "%": 0.01, + "promille": 0.001, + "‰": 0.001 + } + }, + { + "categories": [ + "month_name" + ], + "values": { + "januari": 1, + "jan": 1, + "februari": 2, + "feb": 2, + "mars": 3, + "mar": 3, + "april": 4, + "apr": 4, + "maj": 5, + "juni": 6, + "jun": 6, + "juli": 7, + "jul": 7, + "augusti": 8, + "aug": 8, + "september": 9, + "sep": 9, + "sept": 9, + "oktober": 10, + "okt": 10, + "november": 11, + "nov": 11, + "december": 12, + "dec": 12 + } + }, + { + "categories": [ + "day_of_week" + ], + "values": { + "måndag": 0, + "mån": 0, + "tisdag": 1, + "tis": 1, + "onsdag": 2, + "ons": 2, + "torsdag": 3, + "tor": 3, + "fredag": 4, + "fre": 4, + "lördag": 5, + "lör": 5, + "söndag": 6, + "sön": 6 + } + }, + { + "categories": [ + "noon_midnight_like", + "moment_of_day" + ], + "values": { + "middag": 12, + "midnatt": 0 + } + }, + { + "categories": [ + "moment_of_day" + ], + "values": { + "natt": 3, + "gryning": 6, + "morgon": 9, + "förmiddag": 10, + "lunch": 12, + "eftermiddag": 15, + "kväll": 18, + "skymning": 18, + "sen kväll": 21, + "ikväll": 21, + "ikväll": 23 + } + } + ], + "duration_words": [ + { + "categories": [ + "second" + ], + "values": [ + "sekund", + "sekunder", + "s" + ] + }, + { + "categories": [ + "minute" + ], + "values": [ + "minut", + "minuter", + "min" + ] + }, + { + "categories": [ + "hour" + ], + "values": [ + "timme", + "timmar", + "h" + ] + }, + { + "categories": [ + "day" + ], + "values": [ + "dag", + "dagar", + "dygn" + ] + }, + { + "categories": [ + "week" + ], + "values": [ + "vecka", + "veckor" + ] + }, + { + "categories": [ + "month" + ], + "values": [ + "månad", + "månader" + ] + }, + { + "categories": [ + "year" + ], + "values": [ + "år" + ] + }, + { + "categories": [ + "decade" + ], + "values": [ + "årtionde", + "decennium", + "decennier" + ] + }, + { + "categories": [ + "century" + ], + "values": [ + "århundrade", + "sekel", + "sekler" + ] + }, + { + "categories": [ + "millennium" + ], + "values": [ + "årtusende", + "millennium", + "millennier" + ] + } + ], + "duration_restrict_after_number": [ + "decennium", + "decennier", + "sekel", + "sekler", + "millennium", + "millennier" + ] +} diff --git a/numbers/src/test/resources/config/sv-se/date_time_test.json b/numbers/src/test/resources/config/sv-se/date_time_test.json new file mode 100644 index 00000000..b09e3c52 --- /dev/null +++ b/numbers/src/test/resources/config/sv-se/date_time_test.json @@ -0,0 +1,43 @@ +{ + "test_nice_year": { + "1": {"datetime_param": "1, 1, 31, 13, 22, 3", "bc": "True", "assertEqual": "ett före kristus" }, + "2": {"datetime_param": "10, 1, 31, 13, 22, 3", "bc": "True", "assertEqual": "tio före kristus" }, + "3": {"datetime_param": "92, 1, 31, 13, 22, 3", "bc": "True", "assertEqual": "nittio två före kristus" }, + "4": {"datetime_param": "803, 1, 31, 13, 22, 3", "bc": "None", "assertEqual": "åtta hundra tre" }, + "5": {"datetime_param": "811, 1, 31, 13, 22, 3", "bc": "None", "assertEqual": "åtta hundra elva" }, + "6": {"datetime_param": "454, 1, 31, 13, 22, 3", "bc": "None", "assertEqual": "fyra hundra femtio fyra" }, + "7": {"datetime_param": "1005, 1, 31, 13, 22, 3", "bc": "False", "assertEqual": "ett tusen fem" }, + "8": {"datetime_param": "1012, 1, 31, 13, 22, 3", "bc": "False", "assertEqual": "tio tolv" }, + "9": {"datetime_param": "1046, 1, 31, 13, 22, 3", "bc": "False", "assertEqual": "tio förtio sex" }, + "10": {"datetime_param": "1807, 1, 31, 13, 22, 3", "bc": "None", "assertEqual": "arton noll sju" }, + "11": {"datetime_param": "1717, 1, 31, 13, 22, 3", "bc": "None", "assertEqual": "sjutton sjutton" }, + "12": {"datetime_param": "1988, 1, 31, 13, 22, 3", "bc": "None", "assertEqual": "nitton åttio åtta"}, + "13": {"datetime_param": "2009, 1, 31, 13, 22, 3", "bc": "None", "assertEqual": "två tusen nio"}, + "14": {"datetime_param": "2018, 1, 31, 13, 22, 3", "bc": "None", "assertEqual": "tjugo arton"}, + "15": {"datetime_param": "2021, 1, 31, 13, 22, 3", "bc": "None", "assertEqual": "tjugo tjugo ett"}, + "16": {"datetime_param": "2030, 1, 31, 13, 22, 3", "bc": "None", "assertEqual": "tjugo trettio"}, + "17": {"datetime_param": "2100, 1, 31, 13, 22, 3", "bc": "False", "assertEqual": "tjugo ett hundra" }, + "18": {"datetime_param": "1000, 1, 31, 13, 22, 3", "bc": "None", "assertEqual": "ett tusen" }, + "19": {"datetime_param": "2000, 1, 31, 13, 22, 3", "bc": "None", "assertEqual": "två tusen" }, + "20": {"datetime_param": "3120, 1, 31, 13, 22, 3", "bc": "True", "assertEqual": "trettio ett tjugo före kristus" }, + "21": {"datetime_param": "3241, 1, 31, 13, 22, 3", "bc": "True", "assertEqual": "trettio två förtio ett före kristus" }, + "22": {"datetime_param": "5200, 1, 31, 13, 22, 3", "bc": "False", "assertEqual": "femtio två hundra" }, + "23": {"datetime_param": "1100, 1, 31, 13, 22, 3", "bc": "False", "assertEqual": "elva hundra" }, + "24": {"datetime_param": "2100, 1, 31, 13, 22, 3", "bc": "False", "assertEqual": "tjugo ett hundra" } + }, + "test_nice_date": { + "1": {"datetime_param": "2017, 1, 31, 0, 2, 3", "now": "None", "assertEqual": "tisdag, den trettiförsta januari, tjugo sjutton"}, + "2": {"datetime_param": "2018, 2, 4, 0, 2, 3", "now": "2017, 1, 1, 0, 2, 3", "assertEqual": "söndag, den fjärde februari, tjugo arton"}, + "3": {"datetime_param": "2018, 2, 4, 0, 2, 3", "now": "2018, 1, 1, 0, 2, 3", "assertEqual": "söndag, den fjärde februari"}, + "4": {"datetime_param": "2018, 2, 4, 0, 2, 3", "now": "2018, 2, 1, 0, 2, 3", "assertEqual": "söndag, den fjärde"}, + "5": {"datetime_param": "2018, 2, 4, 0, 2, 3", "now": "2018, 2, 3, 0, 2, 3", "assertEqual": "imorgon"}, + "6": {"datetime_param": "2018, 2, 4, 0, 2, 3", "now": "2018, 2, 4, 0, 2, 3", "assertEqual": "idag"}, + "7": {"datetime_param": "2018, 2, 4, 0, 2, 3", "now": "2018, 2, 5, 0, 2, 3", "assertEqual": "igår"}, + "8": {"datetime_param": "2018, 2, 4, 0, 2, 3", "now": "2018, 2, 6, 0, 2, 3", "assertEqual": "söndag, den fjärde februari"}, + "9": {"datetime_param": "2018, 2, 4, 0, 2, 3", "now": "2019, 2, 6, 0, 2, 3", "assertEqual": "söndag, den fjärde februari, tjugo arton"} + }, + "test_nice_date_time": { + "1": {"datetime_param": "2017, 1, 31, 13, 22, 3", "now": "None", "use_24hour": "False", "use_ampm": "True", "assertEqual": "tisdag, den trettiförsta januari, tjugo sjutton klockan tjugotvå minuter över ett på eftermiddagen"}, + "2": {"datetime_param": "2017, 1, 31, 13, 22, 3", "now": "None", "use_24hour": "True", "use_ampm": "False", "assertEqual": "tisdag, den trettiförsta januari, tjugo sjutton klockan tretton tjugotvå"} + } +} \ No newline at end of file