Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
82.95% covered (warning)
82.95%
73 / 88
57.14% covered (warning)
57.14%
4 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
DigitsToSwedishWords
82.95% covered (warning)
82.95%
73 / 88
57.14% covered (warning)
57.14%
4 / 7
39.73
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 intToOrdinal
66.67% covered (warning)
66.67%
10 / 15
0.00% covered (danger)
0.00%
0 / 1
5.93
 intToWords
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 stringFloatToWords
88.24% covered (warning)
88.24%
15 / 17
0.00% covered (danger)
0.00%
0 / 1
8.10
 getSubDeca
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 assembleWords
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 buildWords
82.22% covered (warning)
82.22%
37 / 45
0.00% covered (danger)
0.00%
0 / 1
13.95
1<?php
2
3namespace MediaWiki\Wikispeech\Segment\TextFilter\Sv;
4
5/**
6 * @file
7 * @ingroup Extensions
8 * @license GPL-2.0-or-later
9 */
10
11use MediaWiki\Logger\LoggerFactory;
12use MediaWiki\Wikispeech\Segment\TextFilter\AbstractDigitsToWords;
13use Psr\Log\LoggerInterface;
14use RuntimeException;
15
16/**
17 * Translates digits to words in Swedish.
18 * Supports gender of the number 1, i.e. 'en' or 'ett',
19 * e.g. the difference between 901 as "niohundra ett" and "niohundra en".
20 * It is however up to the developer to know and select the grammatical context.
21 *
22 * @see DigitsToSwedishWords
23 * @since 0.1.10
24 */
25class DigitsToSwedishWords extends AbstractDigitsToWords {
26
27    /** @var LoggerInterface */
28    private $logger;
29
30    /** @var string[] ordinal text value of values less than 20: first, second... nineteenth. */
31    private const SUB_DECA_ORDINALS = [
32        'PLACEHOLDER FOR INDEX ZERO',
33        'första', 'andra', 'tredje', 'fjärde', 'femte', 'sjätte', 'sjunde', 'åttonde', 'nionde',
34        'tionde', 'elfte', 'tolfte', 'trettonde', 'fjortonde',
35        'femtonde', 'sextonde', 'sjuttonde', 'artonde', 'nittonde'
36    ];
37
38    /** @var string[] text values values less than 20: zero, one, two... nineteen. */
39    private const SUB_DECAS = [
40        'noll',
41        'ONE PLACEHOLDER', 'två', 'tre', 'fyra', 'fem', 'sex', 'sju', 'åtta', 'nio',
42        'tio', 'elva', 'tolv', 'tretton', 'fjorton', 'femton', 'sexton', 'sjutton', 'arton', 'nitton'
43    ];
44
45    /** @var string[] text value of decas: zero, ten, twenty... ninety. */
46    private const DECAS = [
47        'PLACEHOLDER FOR INDEX ZERO',
48        'tio', 'tjugo', 'trettio', 'fyrtio', 'femtio', 'sextio', 'sjuttio', 'åttio', 'nittio'
49    ];
50
51    /**
52     * @var array[] <integer value, text value, definiteness, plural suffix>
53     *
54     * Definiteness is the singular prefix for the definite noun.
55     * I.e. there are two species for 'one' in Swedish: 'en' and 'ett'.
56     */
57    private const MAGNITUDES = [
58        [ 10, 'tio', null, null ],
59        [ 100, 'hundra', 'ett', '' ],
60        [ 1000, 'tusen', 'ett', '' ],
61        [ 1000000, 'miljon', 'en', 'er' ],
62        [ 1000000000, 'miljard', 'en', 'er' ],
63        [ 1000000000000, 'biljon', 'en', 'er' ],
64        [ 1000000000000000, 'biljard', 'en', 'er' ],
65        [ 1000000000000000000, 'triljon', 'en', 'er' ],
66        // TODO: T419005
67        // Modulo can't be used for numbers of this size.
68        // [ 1000000000000000000000, 'triljard', 'en', 'er' ],
69    ];
70
71    /**
72     * @var string There are two genders for 'one' in Swedish: 'en' and 'ett'.
73     * Setting this value to 'en' cause an output such as 'nio hundra en' as in a sum,
74     * while 'ett' cause an output such as 'nio hundra ett' as in the year.
75     */
76    private $one;
77
78    /**
79     * @since 0.1.10
80     * @param string $one There are two species for 'one' in Swedish: 'en' and 'ett'.
81     */
82    public function __construct( string $one = 'ett' ) {
83        $this->one = $one;
84        $this->logger = LoggerFactory::getInstance( 'Wikispeech' );
85    }
86
87    /**
88     * Translate integer to ordinal text value, e.g. 1 -> 'första', 2 -> 'andra'.
89     *
90     * @since 0.1.10
91     * @param int $input
92     * @return string|null Null if input number is not supported
93     */
94    public function intToOrdinal( int $input ): ?string {
95        if ( $input < 1 || $input > 99 ) {
96            // @todo implement support
97            $this->logger->debug( __METHOD__ .
98                ': Input must be greater than 1  and less than 99 but was {input}', [
99                    'input' => $input,
100                ] );
101            return null;
102        }
103        if ( $input < 20 ) {
104            return self::SUB_DECA_ORDINALS[ $input ];
105        }
106        $floor = intval( floor( $input / 10 ) );
107        $word = self::DECAS[ $floor ];
108        $leftovers = $input % 10;
109        if ( $leftovers === 0 ) {
110            $word .= 'nde';
111        } else {
112            $word .= self::SUB_DECA_ORDINALS[ $leftovers ];
113        }
114        return $word;
115    }
116
117    /**
118     * Translate integer to text value, e.g. 1 -> 'ett', 13 -> 'tretton'.
119     *
120     * @since 0.1.10
121     * @param int $input
122     * @return string|null Null if input value is not supported.
123     */
124    public function intToWords( int $input ): ?string {
125        $words = $this->buildWords( $input );
126        if ( $words === null ) {
127            // @todo log?
128        }
129        return $words;
130    }
131
132    /**
133     * Translate floating point to text value, e.g. the floating point ( 3.14 ) -> 'tre komma ett fyra'.
134     *
135     * @since 0.1.10
136     * @param int $integer Integer part of the floating value
137     * @param string|null $decimals Decimals part of the floating value as string value
138     * @return string|null Null if input number is not supported
139     */
140    public function stringFloatToWords(
141        int $integer,
142        ?string $decimals = null
143    ): ?string {
144        // @todo assert decimals are all numbers?
145        if ( $decimals === null ) {
146            return $this->intToWords( $integer );
147        }
148        $integerWords = $this->intToWords( $integer );
149        if ( $integerWords === null ) {
150            // @todo log?
151            return null;
152        }
153        $numberOfDecimals = strlen( $decimals );
154        if ( $numberOfDecimals < 3 && $decimals[0] !== '0' ) {
155            $decimalWords = $this->intToWords( intval( $decimals ) );
156        } else {
157            $decimalWords = '';
158            for ( $decimalIndex = 0; $decimalIndex < $numberOfDecimals; $decimalIndex++ ) {
159                $decimalWord = $this->intToWords( intval( $decimals[$decimalIndex] ) );
160                if ( $decimalWord === null ) {
161                    // @todo log?
162                    return null;
163                }
164                if ( $decimalWords !== '' ) {
165                    $decimalWords .= ' ';
166                }
167                $decimalWords .= $decimalWord;
168            }
169        }
170        return $integerWords . ' komma ' . $decimalWords;
171    }
172
173    /**
174     * @since 0.1.10
175     * @param int $inputNumber
176     * @param array|null $invokingMagnitude
177     * @return string
178     */
179    private function getSubDeca(
180        int $inputNumber,
181        ?array $invokingMagnitude
182    ): string {
183        if ( $invokingMagnitude !== null && $inputNumber === 1 ) {
184            return $invokingMagnitude[2];
185        } elseif ( $inputNumber === 1 ) {
186            return $this->one;
187        } else {
188            return self::SUB_DECAS[ $inputNumber ];
189        }
190    }
191
192    /**
193     * @since 0.1.10
194     * @param array $wordsBuilder
195     * @return string
196     */
197    private function assembleWords( array $wordsBuilder ): string {
198        return implode( ' ', $wordsBuilder );
199    }
200
201    /**
202     * @since 0.1.10
203     * @param int $inputNumber
204     * @param array $wordsBuilder
205     * @param array|null $invokingMagnitude
206     * @return string|null
207     */
208    private function buildWords(
209        int $inputNumber,
210        array $wordsBuilder = [],
211        ?array $invokingMagnitude = null
212    ): ?string {
213        if ( $inputNumber === 0 ) {
214            if ( count( $wordsBuilder ) === 0 ) {
215                return self::SUB_DECAS[0];
216            } else {
217                return $this->assembleWords( $wordsBuilder );
218            }
219        }
220        $leftovers = null;
221        if ( $inputNumber < 20 ) {
222            $wordsBuilder[] = $this->getSubDeca( $inputNumber, $invokingMagnitude );
223            return $this->assembleWords( $wordsBuilder );
224        } elseif ( $inputNumber < 100 ) {
225            $word = self::DECAS[ intval( floor( $inputNumber / 10 ) ) ];
226            $leftovers = $inputNumber % 10;
227            if ( $leftovers > 0 ) {
228                $word .= $this->getSubDeca( $leftovers, $invokingMagnitude );
229                $wordsBuilder[] = $word;
230                return $this->assembleWords( $wordsBuilder );
231            }
232            $wordsBuilder[] = $word;
233        } else {
234            $found = false;
235            $magnitudesCount = count( self::MAGNITUDES );
236            for ( $i = 2; $i < $magnitudesCount; $i++ ) {
237                $magnitude = self::MAGNITUDES[ $i ];
238                if ( $inputNumber < $magnitude[0] ) {
239                    $previousMagnitude = self::MAGNITUDES[ $i - 1 ];
240                    $floor = intval( floor( $inputNumber / $previousMagnitude[0] ) );
241                    $word = $this->buildWords( $floor, [], $previousMagnitude );
242                    if ( $word === null ) {
243                        // Don't log here, this is a recursive action that will flood the log.
244                        // Perhaps log if invokingMagnitude is null.
245                        return null;
246                    }
247                    $word .= ' ';
248                    $word .= $previousMagnitude[1];
249                    if ( $floor > 1 ) {
250                        $word .= $previousMagnitude[3];
251                    }
252                    $wordsBuilder[] = $word;
253                    $leftovers = $inputNumber % $previousMagnitude[0];
254                    $found = true;
255                    break;
256                }
257            }
258            if ( !$found ) {
259                $this->logger->debug( __METHOD__ .
260                    ': Input number is too large to be handled: {inputNumber}', [
261                        'inputNumber' => $inputNumber,
262                    ] );
263                return null;
264            }
265        }
266
267        if ( $leftovers === null ) {
268            throw new RuntimeException( 'Bad code, this should never occur!' );
269        }
270        if ( $leftovers === 0 ) {
271            return $this->assembleWords( $wordsBuilder );
272        }
273        return $this->buildWords( $leftovers, $wordsBuilder, $invokingMagnitude );
274    }
275
276}