77use Lkrms \Support \Date \DateFormatter ;
88use Lkrms \Support \Date \DateFormatterInterface ;
99use DateTimeInterface ;
10- use LogicException ;
1110use Stringable ;
1211
1312/**
2120 */
2221final class Convert extends Utility
2322{
24- /**
25- * Convert a scalar to the type it appears to be
26- *
27- * @param mixed $value
28- * @param bool $toFloat If `true` (the default), convert float strings to
29- * `float`s.
30- * @param bool $toBool If `true` (the default), convert boolean strings to
31- * `bool`s.
32- * @return int|float|string|bool|null
33- */
34- public static function toValue ($ value , bool $ toFloat = true , bool $ toBool = true )
35- {
36- if ($ value === null || is_bool ($ value ) || is_int ($ value ) || is_float ($ value )) {
37- return $ value ;
38- }
39- if (!is_string ($ value )) {
40- throw new LogicException ('$value must be a scalar ' );
41- }
42- if (Pcre::match ('/^ ' . Regex::INTEGER_STRING . '$/ ' , $ value )) {
43- return (int ) $ value ;
44- }
45- if ($ toFloat && is_numeric ($ value )) {
46- return (float ) $ value ;
47- }
48- if ($ toBool && Pcre::match (
49- '/^ ' . Regex::BOOLEAN_STRING . '$/ ' ,
50- $ value ,
51- $ match ,
52- \PREG_UNMATCHED_AS_NULL
53- )) {
54- return $ match ['true ' ] !== null ;
55- }
56- return $ value ;
57- }
58-
59- /**
60- * Convert a value to an integer, preserving null
61- *
62- * @param mixed $value
63- */
64- public static function toInt ($ value ): ?int
65- {
66- if ($ value === null ) {
67- return null ;
68- }
69- return (int ) $ value ;
70- }
71-
72- /**
73- * Expand tabs to spaces
74- */
75- public static function expandTabs (
76- string $ text ,
77- int $ tabSize = 8 ,
78- int $ column = 1
79- ): string {
80- if (strpos ($ text , "\t" ) === false ) {
81- return $ text ;
82- }
83- $ eol = Get::eol ($ text ) ?: "\n" ;
84- $ expanded = '' ;
85- foreach (explode ($ eol , $ text ) as $ i => $ line ) {
86- !$ i || $ expanded .= $ eol ;
87- $ parts = explode ("\t" , $ line );
88- $ last = array_key_last ($ parts );
89- foreach ($ parts as $ p => $ part ) {
90- $ expanded .= $ part ;
91- if ($ p === $ last ) {
92- break ;
93- }
94- $ column += mb_strlen ($ part );
95- // e.g. with $tabSize 4, a tab at $column 2 occupies 3 spaces
96- $ spaces = $ tabSize - (($ column - 1 ) % $ tabSize );
97- $ expanded .= str_repeat (' ' , $ spaces );
98- $ column += $ spaces ;
99- }
100- $ column = 1 ;
101- }
102- return $ expanded ;
103- }
104-
105- /**
106- * Expand leading tabs to spaces
107- */
108- public static function expandLeadingTabs (
109- string $ text ,
110- int $ tabSize = 8 ,
111- bool $ preserveLine1 = false ,
112- int $ column = 1
113- ): string {
114- if (strpos ($ text , "\t" ) === false ) {
115- return $ text ;
116- }
117- $ eol = Get::eol ($ text ) ?: "\n" ;
118- $ softTab = str_repeat (' ' , $ tabSize );
119- $ expanded = '' ;
120- foreach (explode ($ eol , $ text ) as $ i => $ line ) {
121- !$ i || $ expanded .= $ eol ;
122- if ($ i || (!$ preserveLine1 && $ column === 1 )) {
123- $ expanded .= Pcre::replace ('/(?<=\n|\G)\t/ ' , $ softTab , $ line );
124- continue ;
125- }
126- if ($ preserveLine1 ) {
127- $ expanded .= $ line ;
128- continue ;
129- }
130- $ parts = explode ("\t" , $ line );
131- while (($ part = array_shift ($ parts )) !== null ) {
132- $ expanded .= $ part ;
133- if (!$ parts ) {
134- break ;
135- }
136- if ($ part ) {
137- $ expanded .= "\t" . implode ("\t" , $ parts );
138- break ;
139- }
140- $ column += mb_strlen ($ part );
141- $ spaces = $ tabSize - (($ column - 1 ) % $ tabSize );
142- $ expanded .= str_repeat (' ' , $ spaces );
143- $ column += $ spaces ;
144- }
145- }
146- return $ expanded ;
147- }
148-
149- /**
150- * Replace the end of a multi-byte string with an ellipsis ("...") if its
151- * length exceeds a limit
152- */
153- public static function ellipsize (string $ value , int $ length ): string
154- {
155- if (mb_strlen ($ value ) > $ length ) {
156- return rtrim (mb_substr ($ value , 0 , $ length - 3 )) . '... ' ;
157- }
158-
159- return $ value ;
160- }
161-
162- /**
163- * Get a phrase like "between lines 3 and 11" or "on platform 23"
164- *
165- * @param string|null $plural `"{$singular}s"` is used if `$plural` is
166- * `null`.
167- */
168- public static function pluralRange (
169- int $ from ,
170- int $ to ,
171- string $ singular ,
172- ?string $ plural = null ,
173- string $ preposition = 'on '
174- ): string {
175- return $ to - $ from
176- ? sprintf ('between %s %d and %d ' , $ plural === null ? $ singular . 's ' : $ plural , $ from , $ to )
177- : sprintf ('%s %s %d ' , $ preposition , $ singular , $ from );
178- }
179-
18023 /**
18124 * Convert a list of "key=value" strings to an array like ["key" => "value"]
18225 *
@@ -201,177 +44,6 @@ public static function queryToData(array $query): array
20144 );
20245 }
20346
204- /**
205- * Remove duplicates in a string where top-level lines ("sections") are
206- * grouped with "list items" below
207- *
208- * Lines that match `$regex` are regarded as list items, and other lines are
209- * used as the section name for subsequent list items. If `$loose` is
210- * `false` (the default), blank lines between list items clear the current
211- * section name.
212- *
213- * Top-level lines with no children, including any list items orphaned by
214- * blank lines above them, are returned before sections with children.
215- *
216- * If a named subpattern in `$regex` called `indent` matches a non-empty
217- * string, subsequent lines with the same number of spaces for indentation
218- * as there are characters in the match are treated as part of the item,
219- * including any blank lines.
220- *
221- * Line endings used in `$text` may be any combination of LF, CRLF and CR,
222- * but LF (`"\n"`) line endings are used in the return value.
223- *
224- * @param string $separator Used between top-level lines and sections. Has
225- * no effect on the end-of-line sequence used between items, which is always
226- * LF (`"\n"`).
227- * @param string|null $marker Added before each section name. Nested list
228- * items are indented by the equivalent number of spaces. To add a leading
229- * `"- "` to top-level lines and indent others with two spaces, set
230- * `$marker` to `"-"`.
231- * @param bool $clean If `true`, the first match of `$regex` in each section
232- * name is removed.
233- * @param bool $loose If `true`, blank lines between list items are ignored.
234- */
235- public static function linesToLists (
236- string $ text ,
237- string $ separator = "\n" ,
238- ?string $ marker = null ,
239- string $ regex = '/^(?<indent>\h*[-*] )/ ' ,
240- bool $ clean = false ,
241- bool $ loose = false
242- ): string {
243- $ marker = (string ) $ marker !== '' ? $ marker . ' ' : null ;
244- $ indent = $ marker !== null ? str_repeat (' ' , mb_strlen ($ marker )) : '' ;
245- $ markerIsItem = $ marker !== null && Pcre::match ($ regex , $ marker );
246-
247- /** @var array<string,string[]> */
248- $ sections = [];
249- $ lastWasItem = false ;
250- $ lines = Pcre::split ('/\r\n|\n|\r/ ' , $ text );
251- for ($ i = 0 ; $ i < count ($ lines ); $ i ++) {
252- $ line = $ lines [$ i ];
253-
254- // Remove pre-existing markers early to ensure sections with the
255- // same name are combined
256- if ($ marker !== null && !$ markerIsItem && strpos ($ line , $ marker ) === 0 ) {
257- $ line = substr ($ line , strlen ($ marker ));
258- }
259-
260- // Treat blank lines between items as section breaks
261- if (trim ($ line ) === '' ) {
262- if (!$ loose && $ lastWasItem ) {
263- unset($ section );
264- }
265- continue ;
266- }
267-
268- // Collect any subsequent indented lines
269- if (Pcre::match ($ regex , $ line , $ matches )) {
270- $ matchIndent = $ matches ['indent ' ] ?? '' ;
271- if ($ matchIndent !== '' ) {
272- $ matchIndent = str_repeat (' ' , mb_strlen ($ matchIndent ));
273- $ pendingWhitespace = '' ;
274- $ backtrack = 0 ;
275- while ($ i < count ($ lines ) - 1 ) {
276- $ nextLine = $ lines [$ i + 1 ];
277- if (trim ($ nextLine ) === '' ) {
278- $ pendingWhitespace .= $ nextLine . "\n" ;
279- $ backtrack ++;
280- } elseif (substr ($ nextLine , 0 , strlen ($ matchIndent )) === $ matchIndent ) {
281- $ line .= "\n" . $ pendingWhitespace . $ nextLine ;
282- $ pendingWhitespace = '' ;
283- $ backtrack = 0 ;
284- } else {
285- $ i -= $ backtrack ;
286- break ;
287- }
288- $ i ++;
289- }
290- }
291- } else {
292- $ section = $ line ;
293- }
294-
295- $ key = $ section ?? $ line ;
296-
297- if (!array_key_exists ($ key , $ sections )) {
298- $ sections [$ key ] = [];
299- }
300-
301- if ($ key !== $ line ) {
302- if (!in_array ($ line , $ sections [$ key ])) {
303- $ sections [$ key ][] = $ line ;
304- }
305- $ lastWasItem = true ;
306- } else {
307- $ lastWasItem = false ;
308- }
309- }
310-
311- // Move lines with no associated list to the top
312- /** @var array<string,string[]> */
313- $ top = [];
314- $ last = null ;
315- foreach ($ sections as $ section => $ lines ) {
316- if (count ($ lines )) {
317- continue ;
318- }
319-
320- unset($ sections [$ section ]);
321-
322- if ($ clean ) {
323- $ top [$ section ] = [];
324- continue ;
325- }
326-
327- // Collect second and subsequent consecutive top-level list items
328- // under the first so they don't form a loose list
329- if (Pcre::match ($ regex , $ section )) {
330- if ($ last !== null ) {
331- $ top [$ last ][] = $ section ;
332- continue ;
333- }
334- $ last = $ section ;
335- } else {
336- $ last = null ;
337- }
338- $ top [$ section ] = [];
339- }
340- /** @var array<string,string[]> */
341- $ sections = array_merge ($ top , $ sections );
342-
343- $ groups = [];
344- foreach ($ sections as $ section => $ lines ) {
345- if ($ clean ) {
346- $ section = Pcre::replace ($ regex , '' , $ section , 1 );
347- }
348-
349- $ marked = false ;
350- if ($ marker !== null &&
351- !($ markerIsItem && strpos ($ section , $ marker ) === 0 ) &&
352- !Pcre::match ($ regex , $ section )) {
353- $ section = $ marker . $ section ;
354- $ marked = true ;
355- }
356-
357- if (!$ lines ) {
358- $ groups [] = $ section ;
359- continue ;
360- }
361-
362- // Don't separate or indent top-level list items collected above
363- if (!$ marked && Pcre::match ($ regex , $ section )) {
364- $ groups [] = implode ("\n" , [$ section , ...$ lines ]);
365- continue ;
366- }
367-
368- $ groups [] = $ section ;
369- $ groups [] = $ indent . implode ("\n" . $ indent , $ lines );
370- }
371-
372- return implode ($ separator , $ groups );
373- }
374-
37547 /**
37648 * Undo wordwrap(), preserving Markdown-style paragraphs and lists
37749 *
0 commit comments