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        [ 1000000000000000000000, 'triljard', 'en', 'er' ],
67    ];
68
69    /**
70     * @var string There are two genders for 'one' in Swedish: 'en' and 'ett'.
71     * Setting this value to 'en' cause an output such as 'nio hundra en' as in a sum,
72     * while 'ett' cause an output such as 'nio hundra ett' as in the year.
73     */
74    private $one;
75
76    /**
77     * @since 0.1.10
78     * @param string $one There are two species for 'one' in Swedish: 'en' and 'ett'.
79     */
80    public function __construct( string $one = 'ett' ) {
81        $this->one = $one;
82        $this->logger = LoggerFactory::getInstance( 'Wikispeech' );
83    }
84
85    /**
86     * Translate integer to ordinal text value, e.g. 1 -> 'första', 2 -> 'andra'.
87     *
88     * @since 0.1.10
89     * @param int $input
90     * @return string|null Null if input number is not supported
91     */
92    public function intToOrdinal( int $input ): ?string {
93        if ( $input < 1 || $input > 99 ) {
94            // @todo implement support
95            $this->logger->debug( __METHOD__ .
96                ': Input must be greater than 1  and less than 99 but was {input}', [
97                'input' => $input,
98            ] );
99            return null;
100        }
101        if ( $input < 20 ) {
102            return self::SUB_DECA_ORDINALS[ $input ];
103        }
104        $floor = intval( floor( $input / 10 ) );
105        $word = self::DECAS[ $floor ];
106        $leftovers = $input % 10;
107        if ( $leftovers === 0 ) {
108            $word .= 'nde';
109        } else {
110            $word .= self::SUB_DECA_ORDINALS[ $leftovers ];
111        }
112        return $word;
113    }
114
115    /**
116     * Translate integer to text value, e.g. 1 -> 'ett', 13 -> 'tretton'.
117     *
118     * @since 0.1.10
119     * @param int $input
120     * @return string|null Null if input value is not supported.
121     */
122    public function intToWords( int $input ): ?string {
123        $words = $this->buildWords( $input );
124        if ( $words === null ) {
125            // @todo log?
126        }
127        return $words;
128    }
129
130    /**
131     * Translate floating point to text value, e.g. the floating point ( 3.14 ) -> 'tre komma ett fyra'.
132     *
133     * @since 0.1.10
134     * @param int $integer Integer part of the floating value
135     * @param string|null $decimals Decimals part of the floating value as string value
136     * @return string|null Null if input number is not supported
137     */
138    public function stringFloatToWords(
139        int $integer,
140        ?string $decimals = null
141    ): ?string {
142        // @todo assert decimals are all numbers?
143        if ( $decimals === null ) {
144            return $this->intToWords( $integer );
145        }
146        $integerWords = $this->intToWords( $integer );
147        if ( $integerWords === null ) {
148            // @todo log?
149            return null;
150        }
151        $numberOfDecimals = strlen( $decimals );
152        if ( $numberOfDecimals < 3 && $decimals[0] !== '0' ) {
153            $decimalWords = $this->intToWords( intval( $decimals ) );
154        } else {
155            $decimalWords = '';
156            for ( $decimalIndex = 0; $decimalIndex < $numberOfDecimals; $decimalIndex++ ) {
157                $decimalWord = $this->intToWords( intval( $decimals[$decimalIndex] ) );
158                if ( $decimalWord === null ) {
159                    // @todo log?
160                    return null;
161                }
162                if ( $decimalWords !== '' ) {
163                    $decimalWords .= ' ';
164                }
165                $decimalWords .= $decimalWord;
166            }
167        }
168        return $integerWords . ' komma ' . $decimalWords;
169    }
170
171    /**
172     * @since 0.1.10
173     * @param int $inputNumber
174     * @param array|null $invokingMagnitude
175     * @return string
176     */
177    private function getSubDeca(
178        int $inputNumber,
179        ?array $invokingMagnitude
180    ): string {
181        if ( $invokingMagnitude !== null && $inputNumber === 1 ) {
182            return $invokingMagnitude[2];
183        } elseif ( $inputNumber === 1 ) {
184            return $this->one;
185        } else {
186            return self::SUB_DECAS[ $inputNumber ];
187        }
188    }
189
190    /**
191     * @since 0.1.10
192     * @param array $wordsBuilder
193     * @return string
194     */
195    private function assembleWords( array $wordsBuilder ): string {
196        return implode( ' ', $wordsBuilder );
197    }
198
199    /**
200     * @since 0.1.10
201     * @param int $inputNumber
202     * @param array $wordsBuilder
203     * @param array|null $invokingMagnitude
204     * @return string|null
205     */
206    private function buildWords(
207        int $inputNumber,
208        array $wordsBuilder = [],
209        ?array $invokingMagnitude = null
210    ): ?string {
211        if ( $inputNumber === 0 ) {
212            if ( count( $wordsBuilder ) === 0 ) {
213                return self::SUB_DECAS[0];
214            } else {
215                return $this->assembleWords( $wordsBuilder );
216            }
217        }
218        $leftovers = null;
219        if ( $inputNumber < 20 ) {
220            $wordsBuilder[] = $this->getSubDeca( $inputNumber, $invokingMagnitude );
221            return $this->assembleWords( $wordsBuilder );
222        } elseif ( $inputNumber < 100 ) {
223            $word = self::DECAS[ intval( floor( $inputNumber / 10 ) ) ];
224            $leftovers = $inputNumber % 10;
225            if ( $leftovers > 0 ) {
226                $word .= $this->getSubDeca( $leftovers, $invokingMagnitude );
227                $wordsBuilder[] = $word;
228                return $this->assembleWords( $wordsBuilder );
229            }
230            $wordsBuilder[] = $word;
231        } else {
232            $found = false;
233            $magnitudesCount = count( self::MAGNITUDES );
234            for ( $i = 2; $i < $magnitudesCount; $i++ ) {
235                $magnitude = self::MAGNITUDES[ $i ];
236                if ( $inputNumber < $magnitude[0] ) {
237                    $previousMagnitude = self::MAGNITUDES[ $i - 1 ];
238                    $floor = intval( floor( $inputNumber / $previousMagnitude[0] ) );
239                    $word = $this->buildWords( $floor, [], $previousMagnitude );
240                    if ( $word === null ) {
241                        // Don't log here, this is a recursive action that will flood the log.
242                        // Perhaps log if invokingMagnitude is null.
243                        return null;
244                    }
245                    $word .= ' ';
246                    $word .= $previousMagnitude[1];
247                    if ( $floor > 1 ) {
248                        $word .= $previousMagnitude[3];
249                    }
250                    $wordsBuilder[] = $word;
251                    $leftovers = $inputNumber % $previousMagnitude[0];
252                    $found = true;
253                    break;
254                }
255            }
256            if ( !$found ) {
257                $this->logger->debug( __METHOD__ .
258                    ': Input number is too large to be handled: {inputNumber}', [
259                    'inputNumber' => $inputNumber,
260                ] );
261                return null;
262            }
263        }
264
265        if ( $leftovers === null ) {
266            throw new RuntimeException( 'Bad code, this should never occur!' );
267        }
268        if ( $leftovers === 0 ) {
269            return $this->assembleWords( $wordsBuilder );
270        }
271        return $this->buildWords( $leftovers, $wordsBuilder, $invokingMagnitude );
272    }
273
274}