1<?php
2
3/**
4 * This file is part of the ramsey/uuid library
5 *
6 * For the full copyright and license information, please view the LICENSE
7 * file that was distributed with this source code.
8 *
9 * @copyright Copyright (c) Ben Ramsey <ben@benramsey.com>
10 * @license http://opensource.org/licenses/MIT MIT
11 */
12
13declare(strict_types=1);
14
15namespace Ramsey\Uuid\Converter\Time;
16
17use Ramsey\Uuid\Converter\TimeConverterInterface;
18use Ramsey\Uuid\Math\BrickMathCalculator;
19use Ramsey\Uuid\Math\CalculatorInterface;
20use Ramsey\Uuid\Type\Hexadecimal;
21use Ramsey\Uuid\Type\Integer as IntegerObject;
22use Ramsey\Uuid\Type\Time;
23
24use function count;
25use function dechex;
26use function explode;
27use function is_float;
28use function is_int;
29use function str_pad;
30use function strlen;
31use function substr;
32
33use const STR_PAD_LEFT;
34use const STR_PAD_RIGHT;
35
36/**
37 * PhpTimeConverter uses built-in PHP functions and standard math operations
38 * available to the PHP programming language to provide facilities for
39 * converting parts of time into representations that may be used in UUIDs
40 *
41 * @psalm-immutable
42 */
43class PhpTimeConverter implements TimeConverterInterface
44{
45    /**
46     * The number of 100-nanosecond intervals from the Gregorian calendar epoch
47     * to the Unix epoch.
48     */
49    private const GREGORIAN_TO_UNIX_INTERVALS = 0x01b21dd213814000;
50
51    /**
52     * The number of 100-nanosecond intervals in one second.
53     */
54    private const SECOND_INTERVALS = 10000000;
55
56    /**
57     * The number of 100-nanosecond intervals in one microsecond.
58     */
59    private const MICROSECOND_INTERVALS = 10;
60
61    /**
62     * @var CalculatorInterface
63     */
64    private $calculator;
65
66    /**
67     * @var TimeConverterInterface
68     */
69    private $fallbackConverter;
70
71    /**
72     * @var int
73     */
74    private $phpPrecision;
75
76    public function __construct(
77        ?CalculatorInterface $calculator = null,
78        ?TimeConverterInterface $fallbackConverter = null
79    ) {
80        if ($calculator === null) {
81            $calculator = new BrickMathCalculator();
82        }
83
84        if ($fallbackConverter === null) {
85            $fallbackConverter = new GenericTimeConverter($calculator);
86        }
87
88        $this->calculator = $calculator;
89        $this->fallbackConverter = $fallbackConverter;
90        $this->phpPrecision = (int) ini_get('precision');
91    }
92
93    public function calculateTime(string $seconds, string $microseconds): Hexadecimal
94    {
95        $seconds = new IntegerObject($seconds);
96        $microseconds = new IntegerObject($microseconds);
97
98        // Calculate the count of 100-nanosecond intervals since the Gregorian
99        // calendar epoch for the given seconds and microseconds.
100        $uuidTime = ((int) $seconds->toString() * self::SECOND_INTERVALS)
101            + ((int) $microseconds->toString() * self::MICROSECOND_INTERVALS)
102            + self::GREGORIAN_TO_UNIX_INTERVALS;
103
104        // Check to see whether we've overflowed the max/min integer size.
105        // If so, we will default to a different time converter.
106        /** @psalm-suppress RedundantCondition */
107        if (!is_int($uuidTime)) {
108            return $this->fallbackConverter->calculateTime(
109                $seconds->toString(),
110                $microseconds->toString()
111            );
112        }
113
114        return new Hexadecimal(str_pad(dechex((int) $uuidTime), 16, '0', STR_PAD_LEFT));
115    }
116
117    public function convertTime(Hexadecimal $uuidTimestamp): Time
118    {
119        $timestamp = $this->calculator->toInteger($uuidTimestamp);
120
121        // Convert the 100-nanosecond intervals into seconds and microseconds.
122        $splitTime = $this->splitTime(
123            ((int) $timestamp->toString() - self::GREGORIAN_TO_UNIX_INTERVALS)
124            / self::SECOND_INTERVALS
125        );
126
127        if (count($splitTime) === 0) {
128            return $this->fallbackConverter->convertTime($uuidTimestamp);
129        }
130
131        return new Time($splitTime['sec'], $splitTime['usec']);
132    }
133
134    /**
135     * @param int|float $time The time to split into seconds and microseconds
136     *
137     * @return string[]
138     */
139    private function splitTime($time): array
140    {
141        $split = explode('.', (string) $time, 2);
142
143        // If the $time value is a float but $split only has 1 element, then the
144        // float math was rounded up to the next second, so we want to return
145        // an empty array to allow use of the fallback converter.
146        if (is_float($time) && count($split) === 1) {
147            return [];
148        }
149
150        if (count($split) === 1) {
151            return [
152                'sec' => $split[0],
153                'usec' => '0',
154            ];
155        }
156
157        // If the microseconds are less than six characters AND the length of
158        // the number is greater than or equal to the PHP precision, then it's
159        // possible that we lost some precision for the microseconds. Return an
160        // empty array, so that we can choose to use the fallback converter.
161        if (strlen($split[1]) < 6 && strlen((string) $time) >= $this->phpPrecision) {
162            return [];
163        }
164
165        $microseconds = $split[1];
166
167        // Ensure the microseconds are no longer than 6 digits. If they are,
168        // truncate the number to the first 6 digits and round up, if needed.
169        if (strlen($microseconds) > 6) {
170            $roundingDigit = (int) substr($microseconds, 6, 1);
171            $microseconds = (int) substr($microseconds, 0, 6);
172
173            if ($roundingDigit >= 5) {
174                $microseconds++;
175            }
176        }
177
178        return [
179            'sec' => $split[0],
180            'usec' => str_pad((string) $microseconds, 6, '0', STR_PAD_RIGHT),
181        ];
182    }
183}
184