3v4l.org

run code in 300+ PHP versions simultaneously
<?php declare(strict_types = 1); /** * https://github.com/ekinhbayar/IntervalParser/ */ namespace IntervalParser; use \DateInterval; class Exception extends \Exception {} class FormatException extends Exception {} class IntervalIterator implements \Iterator { private $var = []; public function __construct($array) { if (is_array($array)) { $this->var = $array; } } public function rewind() { #echo "rewinding\n"; reset($this->var); } public function current() { $var = current($this->var); # echo "current: $var\n"; return $var; } public function key() { $var = key($this->var); #echo "key: $var\n"; return $var; } public function next() { $var = next($this->var); #echo "next: $var\n"; return $var; } public function valid() { $key = key($this->var); $var = ($key !== NULL && $key !== FALSE); #echo "valid: $var\n"; return $var; } } /** Parser Settings * * IntervalParser takes a ParserSettings object which is handy for when you want to deal with multiple intervals. * * When parsing multiple intervals if you don't supply your own settings, * the parser will try to find comma separated intervals within given input by default. * However there is no default value for $wordSeparator. * * Example: * * $parserSettings = new ParserSettings(',', 'then'); * # works for "2 days then 5 months then 2 hours" * $intervalParser = new IntervalParser($parserSettings); * */ class ParserSettings { const SYMBOL = 0; const STRING = 1; private $separationType; private $symbol; private $word; /** * ParserSettings constructor. * * @param int $separationType * @param string $symbol * @param string|null $word */ public function __construct( int $separationType = self::SYMBOL, string $symbol = ',', string $word = null ) { $this->separationType = $separationType; $this->word = $word; $this->symbol = $symbol; } public function getSymbolSeparator() : string { return $this->symbol; } public function getWordSeparator() : string { return $this->word; } public function getSeparationType(): string { return ($this->separationType == self::STRING) ? 'string' : 'symbol'; } } class IntervalFlags { const INTERVAL_ONLY = 0b00000000; const REQUIRE_TRAILING = 0b00000001; const REQUIRE_LEADING = 0b00000010; const MULTIPLE_INTERVALS = 0b00000100; } class TimeInterval { private $intervalOffset; private $intervalLength; /*private $intervalType;*/ private $leadingData; private $trailingData; private $interval; public function __construct( int $intervalOffset, int $intervalLength, /*int $intervalType,*/ string $leadingData = null, string $trailingData = null, DateInterval $interval ) { $this->interval = $interval; $this->intervalOffset = $intervalOffset; $this->intervalLength = $intervalLength; /*$this->intervalType = $intervalType;*/ $this->leadingData = $leadingData; $this->trailingData = $trailingData; } public function getInterval() : DateInterval { return $this->interval; } public function getIntervalOffset() : int { return $this->intervalOffset; } public function getIntervalLength() : int { return $this->intervalLength; } /*public function getIntervalType() : int { return $this->intervalType; }*/ public function getLeadingData() : string { return $this->leadingData; } public function getTrailingData() : string { return $this->trailingData; } } class IntervalParser { /** * Set of regular expressions utilized to match/replace/validate parts of a given input. * * Don't forget to close parentheses when using $define */ public static $define = "/(?(DEFINE)"; # Definitions of sub patterns for a valid interval public static $integer = <<<'REGEX' (?<integer> (?:\G|(?!\n)) (\s*\b)? \d{1,5} \s* ) REGEX; # Starts with integer followed by time specified public static $timepart = <<<'REGEX' (?<timepart> (?&integer) ( s(ec(ond)?s?)? | m(on(ths?)?|in(ute)?s?)? | h(rs?|ours?)? | d(ays?)? | w(eeks?)? ) ) REGEX; # Leading separator public static $leadingSeparator = "(?<leadingSeparator>\s?(?:in)\s?)"; # Leading separator with capturing groups public static $leadingGroupSeparator = "/(.*)\s+(?:in)\s+(.*)/ui"; # Regex to match a valid interval, holds the value in $matches['interval'] public static $intervalOnly = "^(?<interval>(?&timepart)++)$/uix"; # Regex to match a valid interval and any trailing string, holds the interval in $matches['interval'], the rest in $matches['trailing'] public static $intervalWithTrailingData = "^(?<interval>(?&timepart)++)(?<trailing>.+)*?$/uix"; /** * Used to turn a given non-strtotime-compatible time string into a compatible one * Only modifies the non-strtotime-compatible time strings provided leaving the rest intact * * @var string $normalizerExpression */ public static $normalizerExpression = <<<'REGEX' ~ # grab the integer part of time string \s? (?<int> \d{1,5}) \s? # match only the shortest abbreviations that aren't supported by strtotime (?<time> (?: s (?=(?:ec(?:ond)?s?)?(?:\b|\d)) | m (?=(?:in(?:ute)?s?)?(?:\b|\d)) | h (?=(?:(?:ou)?rs?)?(?:\b|\d)) | d (?=(?:ays?)?(?:\b|\d)) | w (?=(?:eeks?)?(?:\b|\d)) | mon (?=(?:(?:th)?s?)?(?:\b|\d)) ) ) [^\d]*?(?=\b|\d) # do only extract start of string | (?<text> .+) ~uix REGEX; # Regex to handle an input that may have multiple intervals along with leading and/or trailing data public static $multipleIntervals = <<<'REGEX' ^(?<leading>.*?)? (?<sep>(?&leadingSeparator))? (?<interval>(?&timepart)++) (?<trailing>.*) /uix REGEX; /** * IntervalParser constructor. * * Default settings are : * * string $symbolSeparator = ',', * string $wordSeparator = null * * @param \IntervalParser\ParserSettings $settings */ public function __construct(ParserSettings $settings) { $this->settings = $settings; } /** * Looks for a valid interval along with leading and/or trailing data IF the respective flags are set. * TimeInterval is essentially DateInterval with extra information such as interval offset & length, leading/trailing data. * * @param string $input * @param int $flags * @return TimeInterval|array * @throws FormatException */ public function findInterval(string $input, int $flags = IntervalFlags::INTERVAL_ONLY) { if( $flags & ~IntervalFlags::INTERVAL_ONLY & ~IntervalFlags::REQUIRE_TRAILING & ~IntervalFlags::REQUIRE_LEADING & ~IntervalFlags::MULTIPLE_INTERVALS ){ throw new InvalidFlagException("You have tried to use an invalid flag combination."); } if($flags & IntervalFlags::INTERVAL_ONLY){ $input = $this->normalizeTimeInterval($input); $definition = self::$define . self::$integer . self::$timepart .')'; $expression = $definition . self::$intervalOnly; if(preg_match($expression, $input)){ $intervalOffset = 0; $intervalLength = strlen($input); # create and return the interval object $interval = DateInterval::createFromDateString($input); return new TimeInterval($intervalOffset, $intervalLength, null, null, $interval); } throw new FormatException("Given input is not a valid interval."); } if($flags == (IntervalFlags::REQUIRE_LEADING | IntervalFlags::REQUIRE_TRAILING)){ # Default leading separator is "in" $leadingSeparation = preg_match(self::$leadingGroupSeparator, $input, $matches, PREG_OFFSET_CAPTURE); if(!$leadingSeparation){ throw new FormatException("Allowing leading data requires using a separator. Ie. foo in <interval>"); } $leadingData = $matches[1][0] ?? null; $intervalAndTrailingData = $matches[2][0] ?? null; # throw early for missing parts if(!$leadingData){ throw new FormatException("Given input does not contain a valid leading data."); } if(!$intervalAndTrailingData){ throw new FormatException("Given input does not contain a valid interval and/or trailing data."); } $intervalOffset = $matches[2][1] ?? null; # If interval contains non-strtotime-compatible abbreviations, replace 'em $intervalAndTrailingData = $this->normalizeTimeInterval($intervalAndTrailingData); $definition = self::$define . self::$integer . self::$timepart .')'; $expression = $definition . self::$intervalWithTrailingData; if(preg_match($expression, $intervalAndTrailingData, $parts)){ $interval = $parts['interval']; $trailingData = $parts['trailing']; $intervalLength = strlen($interval); # create and return the interval object $interval = DateInterval::createFromDateString($interval); return new TimeInterval($intervalOffset, $intervalLength, $leadingData, $trailingData, $interval); } throw new FormatException("Given input does not contain a valid interval and/or trailing data."); } if($flags & IntervalFlags::REQUIRE_LEADING){ # Default leading separator is "in" $leadingSeparation = preg_match(self::$leadingGroupSeparator, $input, $matches, PREG_OFFSET_CAPTURE); if(!$leadingSeparation){ throw new FormatException("Allowing leading data requires using a separator. Ie. foo in <interval>"); } $leadingData = $matches[1][0] ?? null; $intervalAndPossibleTrailingData = $matches[2][0] ?? null; if(!$leadingData){ throw new FormatException("Could not find any valid leading data."); } if(!$intervalAndPossibleTrailingData){ throw new FormatException("Could not find any valid interval and/or leading data."); } $intervalOffset = $matches[2][1] ?? null; # If interval contains non-strtotime-compatible abbreviations, replace 'em $safeInterval = $this->normalizeTimeInterval($intervalAndPossibleTrailingData); # since above normalization is expected to not return any trailing data, only check for a valid interval $definition = self::$define . self::$integer . self::$timepart .')'; $expression = $definition . self::$intervalOnly; if(preg_match($expression, $safeInterval, $parts)){ $interval = $parts['interval']; $intervalLength = strlen($interval); # create the interval object $interval = DateInterval::createFromDateString($interval); return new TimeInterval($intervalOffset, $intervalLength, $leadingData, null, $interval); } throw new FormatException("Given input does not contain a valid interval. Keep in mind trailing data is not allowed with current flag."); } if($flags & IntervalFlags::REQUIRE_TRAILING){ $definition = self::$define . self::$integer . self::$timepart .')'; $expression = $definition . self::$intervalWithTrailingData; # If interval contains non-strtotime-compatible abbreviations, replace 'em $safeInterval = $this->normalizeTimeInterval($input); # Separate interval from trailing data if(preg_match($expression, $safeInterval, $parts)){ $trailingData = $parts['trailing'] ?? null; $interval = $parts['interval'] ?? null; if(!$interval){ throw new FormatException("Could not find any valid interval."); } if(!$trailingData){ throw new FormatException("Could not find any valid trailing data."); } $intervalLength = strlen($interval); $intervalOffset = 0; # since we don't allow leading data here # create the interval object $interval = DateInterval::createFromDateString($interval); return new TimeInterval($intervalOffset, $intervalLength, null, $trailingData, $interval); } throw new FormatException("Given input does not contain a valid interval. Keep in mind leading data is not allowed with current flag."); } if($flags & IntervalFlags::MULTIPLE_INTERVALS){ $payload = []; $separator = ($this->settings->getSeparationType() == 'symbol') ? $this->settings->getSymbolSeparator() : $this->settings->getWordSeparator(); $expression = "/(?J)\b(?:(?<match>.*?)\s?{$separator})\s?|\b(?<match>.*)/ui"; if(preg_match_all($expression, $input, $intervals, PREG_SET_ORDER)){ $intervalSet = array_filter(array_map(function($set){ foreach($iter = new IntervalIterator($set) as $key => $interval){ if($iter->key() === 'match') { return $interval; } } }, $intervals)); foreach($intervalSet as $key => $interval){ $definition = self::$define . self::$leadingSeparator . self::$integer . self::$timepart .')'; $expression = $definition . self::$multipleIntervals; preg_match($expression, $interval, $matches); $matches = array_filter($matches); print_r($matches); $leadingData = $matches['leading'] ?? null; $leadingSep = $matches['sep'] ?? false; $interval = $matches['interval'] ?? null; $trailing = $matches['trailing'] ?? null; if(!$leadingData){ $leadingData = ($leadingSep) ?: ""; } $intervalOffset = (!$leadingSep) ? 0 : strlen($leadingData) + strlen($leadingSep); # If interval contains non-strtotime-compatible abbreviations, replace them $safeInterval = $this->normalizeTimeInterval($interval . $trailing); # Separate intervals from trailing data if(preg_match($expression, $safeInterval, $parts)){ $trailingData = $parts['trailing'] ?? null; $interval = $parts['interval'] ?? null; if(!$interval) continue; $intervalLength = strlen($interval); # create the interval object $interval = DateInterval::createFromDateString($interval); $payload[] = new TimeInterval($intervalOffset, $intervalLength, $leadingData, $trailingData, $interval); } } if($payload) return $payload; } } } /** * Turns any non-strtotime-compatible time string into a compatible one. * If the passed input has trailing data, it won't be lost since within the callback the input is reassembled. * However no leading data is accepted. * * @param string $input * @return string */ public function normalizeTimeInterval(string $input): string { $output = preg_replace_callback(self::$normalizerExpression, function ($matches) { $int = $matches['int']; switch ($matches['time']) { case 's': $t = ($int == 1) ? ' second ' : ' seconds '; break; case 'm': $t = ($int == 1) ? ' minute ' : ' minutes '; break; case 'h': $t = ($int == 1) ? ' hour ' : ' hours '; break; case 'd': $t = ($int == 1) ? ' day ' : ' days '; break; case 'w': $t = ($int == 1) ? ' week ' : ' weeks '; break; case 'mon': $t = ($int == 1) ? ' month ' : ' months '; break; case 'y': $t = ($int == 1) ? ' year ' : ' years '; break; } $t = $t ?? ''; # rebuild the interval string $time = $int . $t; if(isset($matches['text'])){ $time .= trim($matches['text']); } return $time; }, $input); return trim($output); } /** * Normalizes any non-strtotime-compatible time string, then validates the interval and returns a DateInterval object. * No leading or trailing data is accepted. * * @param string $input * @return DateInterval * @throws \InvalidArgumentException */ public function parseInterval(string $input): \DateInterval { $input = trim($this->normalizeTimeInterval($input)); $definition = self::$define . self::$integer . self::$timepart .')'; $expression = $definition . self::$intervalOnly; if(preg_match($expression, $input, $matches)){ return DateInterval::createFromDateString($input); } throw new FormatException("Given string is not a valid time interval."); } } # Set ParserSettings for IntervalParser $settings1 = new ParserSettings(); $settings2 = new ParserSettings(0); $settings3 = new ParserSettings(1, ',', 'then'); $settings4 = new ParserSettings(0, '-'); $intervalParser1 = new IntervalParser($settings1); $intervalParser2 = new IntervalParser($settings2); $intervalParser3 = new IntervalParser($settings3); $intervalParser4 = new IntervalParser($settings4); // Usage # Only interval & trailing data $trailingString = '7mon6w5d4h3m2s bazinga!'; #$timeIntervalWithTrailingData = $intervalParser->findInterval($trailingString, IntervalFlags::REQUIRE_TRAILING); #var_dump($timeIntervalWithTrailingData); # Only leading data & interval $leadingString = 'foo in 9w8d7h6m5s'; #$timeIntervalWithLeadingData = $intervalParser->findInterval($leadingString, IntervalFlags::REQUIRE_LEADING); #var_dump($timeIntervalWithLeadingData); # Both interval & leading & trailing data $both = 'foo in 9d8h5m bar'; #$timeIntervalWithBoth = $intervalParser->findInterval($both, IntervalFlags::REQUIRE_TRAILING | IntervalFlags::REQUIRE_LEADING); #var_dump($timeIntervalWithBoth); # Only interval $onlyInterval = '9 mon 2 w 3 m 4 d'; #$dateInterval = $intervalParser->parseInterval($onlyInterval); #var_dump($dateInterval); #$normalizedInterval = $intervalParser->normalizeTimeInterval($onlyInterval); #var_dump($normalizedInterval); # Multiple Intervals # 1. Comma Separated $multiple = 'foo in 9d8h5m bar , baz in 5 minutes, foo in 2 days 4 minutes boo, in 1 hr, 10 days'; $multipleWithSymbol = 'foo in 9d8h5m bar - baz in 5 minutes- foo in 2 days 4 minutes boo- in 1 hr- 10 days'; $multipleWithWord = 'foo in 9d8h5m bar then baz in 5 minutes then foo in 2 days 4 minutes boo then in 1 hr then 10 days'; $multipleIntervals = $intervalParser1->findInterval($multiple, IntervalFlags::MULTIPLE_INTERVALS); var_dump($multipleIntervals); # 2. Separated by a defined-on-settings word #$wordSeparated = 'foo in 9d8h5m bar then baz in 5 minutes then foo in 2 days 4 minutes boo then in 2 hours'; #$wordSeparatedIntervals = $intervalParser3->findInterval($wordSeparated, IntervalFlags::MULTIPLE_INTERVALS); #var_dump($wordSeparatedIntervals);

Here you find the average performance (time & memory) of each version. A grayed out version indicates it didn't complete successfully (based on exit-code).

VersionSystem time (s)User time (s)Memory (MiB)
8.3.70.0140.00718.80
8.3.60.0100.01317.38
8.3.50.0320.01122.10
8.3.40.0170.00719.46
8.3.30.0100.00619.42
8.3.20.0120.00619.52
8.3.10.0070.01421.86
8.3.00.0000.00922.03
8.2.190.0060.01017.38
8.2.180.0070.01025.92
8.2.170.0100.00619.57
8.2.160.0100.01022.96
8.2.150.0060.00325.66
8.2.140.0060.00324.66
8.2.130.0000.00926.16
8.2.120.0060.00320.11
8.2.110.0060.00621.31
8.2.100.0060.00618.53
8.2.90.0090.00019.67
8.2.80.0030.00618.13
8.2.70.0030.00618.25
8.2.60.0030.00618.25
8.2.50.0070.00318.13
8.2.40.0070.00319.97
8.2.30.0060.00321.30
8.2.20.0040.00418.54
8.2.10.0030.00618.61
8.2.00.0000.00918.40
8.1.280.0100.00725.92
8.1.270.0030.00724.66
8.1.260.0070.00326.35
8.1.250.0090.00028.09
8.1.240.0070.00421.68
8.1.230.0000.01318.20
8.1.220.0000.00918.04
8.1.210.0030.00718.77
8.1.200.0030.00717.98
8.1.190.0000.00917.98
8.1.180.0000.00918.10
8.1.170.0000.00917.97
8.1.160.0090.00022.38
8.1.150.0000.00919.31
8.1.140.0000.01020.07
8.1.130.0060.00318.04
8.1.120.0030.00618.09
8.1.110.0040.00418.06
8.1.100.0040.00418.05
8.1.90.0030.00618.02
8.1.80.0000.00917.95
8.1.70.0060.00318.11
8.1.60.0060.00318.14
8.1.50.0060.00317.97
8.1.40.0030.00717.98
8.1.30.0000.00918.14
8.1.20.0060.00318.13
8.1.10.0070.00318.09
8.1.00.0040.00418.07
8.0.300.0030.00618.77
8.0.290.0070.00317.50
8.0.280.0030.00618.81
8.0.270.0030.00517.68
8.0.260.0000.00817.60
8.0.250.0040.00417.74
8.0.240.0090.00317.67
8.0.230.0070.00417.64
8.0.220.0120.00017.59
8.0.210.0000.00917.69
8.0.200.0040.00417.73
8.0.190.0040.00417.68
8.0.180.0060.00317.73
8.0.170.0040.00417.69
8.0.160.0030.00617.59
8.0.150.0030.00617.56
8.0.140.0030.00617.46
8.0.130.0000.00714.09
8.0.120.0030.00717.63
8.0.110.0000.00817.70
8.0.100.0090.00017.51
8.0.90.0000.00917.70
8.0.80.0070.01117.70
8.0.70.0090.00017.56
8.0.60.0000.01017.51
8.0.50.0030.00617.71
8.0.30.0170.01017.85
8.0.20.0160.01017.77
8.0.10.0000.01017.70
8.0.00.0110.01217.62
7.4.330.0030.00315.55
7.4.320.0030.00617.27
7.4.300.0040.00417.17
7.4.290.0040.00717.26
7.4.280.0060.00317.27
7.4.270.0060.00317.20
7.4.260.0000.00817.26
7.4.250.0090.00017.18
7.4.240.0000.00917.27
7.4.230.0000.00817.35
7.4.220.0150.00917.21
7.4.210.0100.01617.20
7.4.200.0030.00517.20
7.4.160.0090.01217.23
7.4.150.0000.02517.40
7.4.140.0160.01117.86
7.4.130.0130.01217.11
7.4.120.0170.00717.13
7.4.110.0120.00917.29
7.4.100.0170.00617.09
7.4.90.0120.00917.32
7.4.80.0090.01519.39
7.4.70.0040.01617.24
7.4.60.0040.01917.18
7.4.50.0030.01617.01
7.4.40.0110.01417.14
7.4.30.0110.00917.46
7.4.00.0070.01015.38
7.3.330.0050.00313.77
7.3.320.0030.00313.77
7.3.310.0060.00317.04
7.3.300.0000.00817.02
7.3.290.0090.01216.99
7.3.280.0120.00917.01
7.3.270.0170.00817.40
7.3.260.0140.01617.22
7.3.250.0120.01117.13
7.3.240.0070.01617.14
7.3.230.0170.00717.00
7.3.210.0090.01417.25
7.3.200.0120.01517.12
7.3.190.0160.00617.20
7.3.180.0150.00617.29
7.3.170.0120.01317.03
7.3.160.0070.01517.16
7.3.00.0070.00717.06
7.2.330.0130.01017.33
7.2.320.0110.01417.28
7.2.310.0130.00917.28
7.2.300.0070.02117.18
7.2.290.0130.01017.15
7.2.130.0000.01817.17
7.2.120.0030.01017.48
7.2.110.0030.00917.12
7.2.100.0120.00817.42
7.2.90.0070.01317.25
7.2.80.0030.00917.40
7.2.70.0050.00917.46
7.2.60.0080.01017.44
7.2.50.0130.00017.18
7.2.40.0160.00317.51
7.2.30.0070.01017.29
7.2.20.0030.00717.28
7.2.10.0090.00617.45
7.2.00.0030.01317.50
7.1.250.0090.00616.26
7.1.240.0000.01516.14
7.1.230.0080.00816.24
7.1.220.0030.01316.27
7.1.210.0120.00916.09
7.1.200.0070.00516.24
7.1.190.0090.00316.12
7.1.180.0080.01115.99
7.1.170.0000.01616.28
7.1.160.0030.01416.08
7.1.150.0060.00916.25
7.1.140.0100.01016.23
7.1.130.0080.00416.36
7.1.120.0070.00716.25
7.1.110.0060.01016.33
7.1.100.0030.00816.12
7.1.90.0070.00716.35
7.1.80.0070.00716.35
7.1.70.0030.00916.88
7.1.60.0050.00916.99
7.1.50.0020.01716.96
7.1.40.0060.00915.98
7.1.30.0110.00416.30
7.1.20.0000.02116.29
7.1.10.0110.00716.20
7.1.00.0150.03719.29
7.0.330.0060.00815.79
7.0.320.0030.01215.80
7.0.310.0090.00315.86
7.0.300.0090.00615.61
7.0.290.0070.01115.56
7.0.280.0030.01715.75
7.0.270.0050.01115.55
7.0.260.0070.00715.50
7.0.250.0030.01115.79
7.0.240.0080.00915.79
7.0.230.0060.00615.68
7.0.220.0120.00615.79
7.0.210.0100.00715.79
7.0.200.0050.01416.38
7.0.190.0060.00615.88
7.0.180.0140.00715.63
7.0.170.0140.00015.79
7.0.160.0090.00915.88
7.0.150.0080.00815.82
7.0.140.0070.01015.79
7.0.130.0030.01415.97
7.0.120.0550.03818.21
7.0.110.0660.03118.15
7.0.100.0830.03118.07
7.0.90.0500.03818.05
7.0.80.0910.03018.12
7.0.70.0180.03318.14
7.0.60.0150.03217.97
7.0.50.0730.04018.10
7.0.40.0240.02817.13
7.0.30.0190.03217.11
7.0.20.0230.02717.01
7.0.10.0650.03517.12
7.0.00.0210.03717.13
5.6.380.0000.01214.29
5.6.370.0000.01314.23
5.6.360.0080.00313.88
5.6.350.0160.00014.36
5.6.340.0120.00313.99
5.6.330.0070.00714.34
5.6.320.0110.00013.82
5.6.310.0080.00314.21
5.6.300.0000.01014.02
5.6.290.0030.00913.70
5.6.280.0070.00314.11
5.6.270.0100.03317.60
5.6.260.0030.03417.51
5.6.250.0080.03317.46
5.6.240.0130.02717.56
5.6.230.0070.03217.51
5.6.220.0070.03017.55
5.6.210.0030.03517.53
5.6.200.0090.03117.43
5.6.190.0030.03517.55
5.6.180.0080.03317.54
5.6.170.0140.02517.71
5.6.160.0070.03717.65
5.6.150.0030.04517.42
5.6.140.0130.03517.57
5.6.130.0140.03617.59
5.6.120.0070.04917.53
5.6.110.0100.03517.55
5.6.100.0120.04317.52
5.6.90.0150.02517.43
5.6.80.0090.03517.05
5.6.70.0030.04117.13
5.6.60.0060.04317.14
5.6.50.0100.03317.35
5.6.40.0120.02417.07
5.6.30.0080.02817.08
5.6.20.0090.02817.23
5.6.10.0060.03417.20
5.6.00.0030.03617.24

preferences:
54.04 ms | 401 KiB | 5 Q