暂存
This commit is contained in:
commit
b8f599a617
9
.env
Normal file
9
.env
Normal file
@ -0,0 +1,9 @@
|
||||
LLM_API_BASE_URL="https://open.bigmodel.cn/api/paas/v4"
|
||||
LLM_API_KEY="469ff60878bb4d5b9b76577635118c1e.zsHPoKkDv5XEzpkl"
|
||||
LLM_CHAT_MODEL="glm-4.7-flash"
|
||||
LLM_CHAT_MAX_TOKENS=5120
|
||||
LLM_CHAT_TEMPERATURE=0.2
|
||||
LLM_METADATA_ENABLED="true"
|
||||
LLM_METADATA_MODEL="glm-4.7-flash"
|
||||
LLM_METADATA_MAX_TOKENS=2480
|
||||
LLM_METADATA_TEMPERATURE=0.1
|
||||
18
Dockerfile
Normal file
18
Dockerfile
Normal file
@ -0,0 +1,18 @@
|
||||
FROM php:8.3.22-cli-alpine
|
||||
|
||||
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
|
||||
|
||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
|
||||
&& apk update --no-cache \
|
||||
&& docker-php-source extract
|
||||
|
||||
# install extensions
|
||||
RUN docker-php-ext-install pdo pdo_mysql -j$(nproc) pcntl
|
||||
|
||||
# enable opcache and pcntl
|
||||
RUN docker-php-ext-enable opcache pcntl
|
||||
RUN docker-php-source delete \
|
||||
rm -rf /var/cache/apk/*
|
||||
|
||||
RUN mkdir -p /app
|
||||
WORKDIR /app
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 walkor<walkor@workerman.net> and contributors (see https://github.com/walkor/webman/contributors)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
277
apidoc/importapi.md
Normal file
277
apidoc/importapi.md
Normal file
@ -0,0 +1,277 @@
|
||||
# 档案导入 API
|
||||
|
||||
## 接口说明
|
||||
|
||||
导入一份 Markdown 档案文本,将其规范化为 Archive、Page、Chunk 三层结构,并为每个 chunk 生成全局唯一的 `chunk_uid`。
|
||||
|
||||
正式使用时,推荐直接上传原始 Markdown 文件,或者把 Markdown 原文作为请求体发送。系统会根据 Markdown 中的分页标记自动分页、分段和切 chunk。
|
||||
|
||||
导入会先写入 PostgreSQL。如果 `title/year/author/tags/summary` 缺失,系统会把 `archive_uid` 推入 Redis 队列,由独立的 `ai_metadata` process 异步请求 AI 补全并更新数据库。
|
||||
|
||||
当前版本会把导入结果保存为运行时快照文件:
|
||||
|
||||
```text
|
||||
runtime/proofdb/imports/{import_uid}.json
|
||||
```
|
||||
|
||||
后续接入 MySQL、OpenSearch、Vector DB 时,会沿用当前返回结构中的 UID。
|
||||
|
||||
## 请求方式
|
||||
|
||||
```http
|
||||
POST /api/articles/import
|
||||
```
|
||||
|
||||
支持三种调用方式:
|
||||
|
||||
1. `multipart/form-data` 上传 Markdown 文件。
|
||||
2. `text/markdown` 或 `text/plain` 直接发送 Markdown 原文。
|
||||
3. `application/json` 发送 Markdown 字符串,适合程序内部调用。
|
||||
|
||||
## 请求字段
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `file` | file | 否 | Markdown 文件。使用 `multipart/form-data` 时推荐传此字段 |
|
||||
| `archive_uid` | string | 否 | 档案级 ULID。不传时系统自动生成 |
|
||||
| `title` | string | 否 | 档案标题。不传时,系统会优先请求 AI 生成,失败后从 Markdown 标题或文件名推断 |
|
||||
| `year` | int | 否 | 档案年份。不传时,系统会请求 AI 从文本中抽取或推断 |
|
||||
| `author` | string | 否 | 作者或签发人。不传时,系统会请求 AI 从文本中抽取或推断 |
|
||||
| `summary` | string | 否 | 档案摘要。不传时,系统会请求 AI 生成 |
|
||||
| `source` | string | 否 | 来源标识。不传时,系统会使用上传文件名或 `raw-markdown` |
|
||||
| `series` | string | 否 | 所属系列集 |
|
||||
| `tags` | array | 否 | 标签数组。不传时,系统会请求 AI 生成 |
|
||||
| `metadata` | object | 否 | 档案级元数据,例如年份、作者、馆藏信息 |
|
||||
| `content` | string | 否 | Markdown 原文。JSON 调用时使用 |
|
||||
| `pages` | array | 否 | 页数组。兼容字段,一般不需要调用方传入 |
|
||||
| `pages[].page_number` | int|string | 是 | 页码,可传数字或原始页码字符串,例如 `12`、`12a`、`封面` |
|
||||
| `pages[].content` | string | 是 | 当前页 OCR 或转录文本 |
|
||||
| `pages[].metadata` | object | 否 | 页级元数据 |
|
||||
| `paragraphs` | array | 否 | 段落数组。兼容字段,一般不需要调用方传入 |
|
||||
| `paragraphs[].page_number` | int|string | 否 | 当前段落所在页码 |
|
||||
| `paragraphs[].content` | string | 是 | 段落正文 |
|
||||
| `paragraphs[].metadata` | object | 否 | 段落级元数据 |
|
||||
| `chunk_size` | int | 否 | 每个 chunk 的目标最大字符数,默认 `800`,范围 `100-4000` |
|
||||
| `chunk_overlap` | int | 否 | 超长文本硬切时的重叠字符数,默认 `120`,必须小于 `chunk_size` |
|
||||
|
||||
## Markdown 分页规则
|
||||
|
||||
系统会优先识别以下分页格式:
|
||||
|
||||
```markdown
|
||||
<!-- DOCMASTER:PAGE 0001 -->
|
||||
|
||||
## Page 1
|
||||
|
||||
第一页正文...
|
||||
|
||||
---
|
||||
|
||||
<!-- DOCMASTER:PAGE 0002 -->
|
||||
|
||||
## Page 2
|
||||
|
||||
第二页正文...
|
||||
```
|
||||
|
||||
如果没有 `DOCMASTER:PAGE` 标记,系统会把整份 Markdown 当作单页处理。
|
||||
|
||||
## 分段与 Chunk 切分策略
|
||||
|
||||
当前 import 使用 Markdown 预处理 + 页内向量 chunk 切分:
|
||||
|
||||
1. 先根据 Markdown 分页标记拆成 page。
|
||||
2. 每页内部去掉分页标题、水平分隔线等结构性标记。
|
||||
3. 页眉、页脚、密级、纯页码等噪声块会被过滤,不作为向量 chunk 的正文。
|
||||
4. 检索证据只定位到页码,因此 chunk 可以跨段落、跨列表项,但不会跨页。
|
||||
5. 每页内部会按段落/句子/自然停顿形成文本单元,并尽量打包到接近 `chunk_size`。
|
||||
6. 如果单个文本单元超过 `chunk_size`,才退回固定字符窗口硬切,并使用 `chunk_overlap` 保留上下文。
|
||||
|
||||
## 请求示例
|
||||
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:8787/api/articles/import \
|
||||
-F 'title=NSD 76 Disposition of NSC Policy Documents' \
|
||||
-F 'source=archive://nsc/nsd-76' \
|
||||
-F 'chunk_size=800' \
|
||||
-F 'chunk_overlap=120' \
|
||||
-F 'file=@test/1.test.md;type=text/markdown'
|
||||
```
|
||||
|
||||
也可以直接发送 Markdown 原文:
|
||||
|
||||
```bash
|
||||
curl -X POST 'http://127.0.0.1:8787/api/articles/import?title=NSD%2076&source=archive://nsc/nsd-76' \
|
||||
-H 'Content-Type: text/markdown' \
|
||||
--data-binary '@test/1.test.md'
|
||||
```
|
||||
|
||||
JSON 调用示例:
|
||||
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:8787/api/articles/import \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"title": "NSD 76 Disposition of NSC Policy Documents",
|
||||
"source": "archive://nsc/nsd-76",
|
||||
"content": "<!-- DOCMASTER:PAGE 0001 -->\n\n## Page 1\n\nMarkdown 正文..."
|
||||
}'
|
||||
```
|
||||
|
||||
## 成功响应
|
||||
|
||||
状态码:
|
||||
|
||||
```http
|
||||
201 Created
|
||||
```
|
||||
|
||||
响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "Archive imported.",
|
||||
"data": {
|
||||
"import_uid": "01HX8K7V9Y3QF2Z6M4A1B8C9D0",
|
||||
"archive": {
|
||||
"archive_uid": "01HX8K7V9Y3QF2Z6M4A1B8C9D0",
|
||||
"title": "NSD 76 Disposition of NSC Policy Documents",
|
||||
"year": 1992,
|
||||
"author": "Brent Scowcroft",
|
||||
"source": "archive://nsc/nsd-76",
|
||||
"series": null,
|
||||
"tags": [
|
||||
"美国国家安全委员会",
|
||||
"政策文件",
|
||||
"NSD 76"
|
||||
],
|
||||
"summary": "这份档案是 1992 年关于 NSC 政策文件处置的国家安全指令,列明已被取代或已完成、不再有效的政策文件。",
|
||||
"metadata": {
|
||||
"ai_enrichment": {
|
||||
"enabled": true,
|
||||
"attempted": true,
|
||||
"filled": [
|
||||
"year",
|
||||
"author",
|
||||
"tags",
|
||||
"summary"
|
||||
],
|
||||
"missing": [],
|
||||
"model": "glm-4.7-flash"
|
||||
}
|
||||
}
|
||||
},
|
||||
"pages": [
|
||||
{
|
||||
"page_number": 1,
|
||||
"block_count": 8,
|
||||
"chunk_count": 2,
|
||||
"content_length": 1042,
|
||||
"chunk_uids": [
|
||||
"01HX8K7V9Y3QF2Z6M4A1B8C9D0_1_28172",
|
||||
"01HX8K7V9Y3QF2Z6M4A1B8C9D0_2_19304"
|
||||
]
|
||||
}
|
||||
],
|
||||
"chunks": [
|
||||
{
|
||||
"chunk_uid": "01HX8K7V9Y3QF2Z6M4A1B8C9D0_1_28172",
|
||||
"chunk_index": 1,
|
||||
"page_start": 1,
|
||||
"page_end": 1,
|
||||
"pages": [
|
||||
1
|
||||
],
|
||||
"text": "NSD 45 20 AUG 90 U.S. Policy in Response to the Iraqi Invasion of Kuwait (C)\n* *COMMENT** OBE by operations Desert Shield and Desert Storm.",
|
||||
"length": 142,
|
||||
"embedding_ref": null
|
||||
}
|
||||
],
|
||||
"stats": {
|
||||
"page_count": 8,
|
||||
"page_block_count": 86,
|
||||
"chunk_count": 14,
|
||||
"chunk_size": 800,
|
||||
"chunk_overlap": 120
|
||||
},
|
||||
"queue": {
|
||||
"ai_metadata_enqueued": true,
|
||||
"needs_ai_metadata": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 错误响应
|
||||
|
||||
### JSON 格式错误
|
||||
|
||||
状态码:
|
||||
|
||||
```http
|
||||
400 Bad Request
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Invalid JSON body.",
|
||||
"errors": {
|
||||
"body": "Syntax error"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 参数校验失败
|
||||
|
||||
状态码:
|
||||
|
||||
```http
|
||||
422 Unprocessable Entity
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 422,
|
||||
"message": "Archive import validation failed.",
|
||||
"errors": {
|
||||
"source": [
|
||||
"source is required."
|
||||
],
|
||||
"content": [
|
||||
"content, file, pages, or paragraphs is required."
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 快照保存失败
|
||||
|
||||
状态码:
|
||||
|
||||
```http
|
||||
500 Internal Server Error
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 500,
|
||||
"message": "Archive import snapshot could not be saved.",
|
||||
"errors": {
|
||||
"storage": "具体错误信息"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## UID 说明
|
||||
|
||||
| UID | 说明 |
|
||||
| --- | --- |
|
||||
| `import_uid` | 本次导入任务 ID,目前等同于档案级 ULID |
|
||||
| `archive_uid` | 档案级 ULID,例如 `01HX8K7V9Y3QF2Z6M4A1B8C9D0` |
|
||||
| `chunk_uid` | chunk 级核心 ID,格式为 `{archive_uid}_{chunk_index}_{short_uid}` |
|
||||
|
||||
`chunk_uid` 是后续 MySQL、OpenSearch、Vector DB 之间关联的核心字段。
|
||||
|
||||
`chunk_index` 从 `1` 开始递增。`short_uid` 是 5 位数字短码,由 chunk 的稳定内容哈希生成,方便人工查看和引用。
|
||||
133
app/controller/Api/ArticleImportController.php
Normal file
133
app/controller/Api/ArticleImportController.php
Normal file
@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace app\controller\Api;
|
||||
|
||||
use app\service\ArticleImportService;
|
||||
use JsonException;
|
||||
use support\Request;
|
||||
use support\Response;
|
||||
use Webman\Http\UploadFile;
|
||||
use Throwable;
|
||||
|
||||
class ArticleImportController
|
||||
{
|
||||
public function import(Request $request): Response
|
||||
{
|
||||
try {
|
||||
$payload = $this->payload($request);
|
||||
} catch (JsonException $exception) {
|
||||
return $this->jsonResponse([
|
||||
'code' => 400,
|
||||
'message' => 'Invalid JSON body.',
|
||||
'errors' => ['body' => $exception->getMessage()],
|
||||
], 400);
|
||||
}
|
||||
|
||||
$service = new ArticleImportService();
|
||||
$result = $service->import($payload);
|
||||
|
||||
if (!$result['ok']) {
|
||||
return $this->jsonResponse([
|
||||
'code' => 422,
|
||||
'message' => 'Archive import validation failed.',
|
||||
'errors' => $result['errors'],
|
||||
], 422);
|
||||
}
|
||||
|
||||
try {
|
||||
$service->persistSnapshot($result['data']);
|
||||
(new \app\service\ArchiveRepository())->saveImport($result['data']);
|
||||
if (($result['data']['queue']['needs_ai_metadata'] ?? false) === true) {
|
||||
(new \app\service\AiMetadataQueue())->push($result['data']['archive']['archive_uid']);
|
||||
$result['data']['queue']['ai_metadata_enqueued'] = true;
|
||||
}
|
||||
} catch (Throwable $exception) {
|
||||
return $this->jsonResponse([
|
||||
'code' => 500,
|
||||
'message' => 'Archive import data could not be saved.',
|
||||
'errors' => ['storage' => $exception->getMessage()],
|
||||
], 500);
|
||||
}
|
||||
|
||||
return $this->jsonResponse([
|
||||
'code' => 0,
|
||||
'message' => 'Archive imported.',
|
||||
'data' => $result['data'],
|
||||
], 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws JsonException
|
||||
*/
|
||||
private function payload(Request $request): array
|
||||
{
|
||||
$payload = array_merge($request->get(), $request->post());
|
||||
$file = $request->file('file');
|
||||
|
||||
if ($file instanceof UploadFile) {
|
||||
if (!$file->isValid()) {
|
||||
return $payload + ['file_error' => $file->getUploadErrorCode()];
|
||||
}
|
||||
|
||||
$payload['content'] = file_get_contents($file->getPathname()) ?: '';
|
||||
$payload['source'] = $payload['source'] ?? $file->getUploadName();
|
||||
$payload['title'] = $payload['title'] ?? pathinfo($file->getUploadName() ?? '', PATHINFO_FILENAME);
|
||||
$payload['metadata'] = $this->metadata($payload['metadata'] ?? null);
|
||||
return $payload;
|
||||
}
|
||||
|
||||
$rawBody = trim($request->rawBody());
|
||||
if ($rawBody === '') {
|
||||
$payload['metadata'] = $this->metadata($payload['metadata'] ?? null);
|
||||
return $payload;
|
||||
}
|
||||
|
||||
if ($this->isJsonRequest($request)) {
|
||||
$jsonPayload = json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
|
||||
if (!is_array($jsonPayload)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$jsonPayload['metadata'] = $this->metadata($jsonPayload['metadata'] ?? null);
|
||||
return $jsonPayload;
|
||||
}
|
||||
|
||||
$payload['content'] = $rawBody;
|
||||
$payload['source'] = $payload['source'] ?? 'raw-markdown';
|
||||
$payload['metadata'] = $this->metadata($payload['metadata'] ?? null);
|
||||
return $payload;
|
||||
}
|
||||
|
||||
private function isJsonRequest(Request $request): bool
|
||||
{
|
||||
$contentType = strtolower($request->header('content-type', ''));
|
||||
return str_contains($contentType, 'application/json') || str_contains($contentType, '+json');
|
||||
}
|
||||
|
||||
private function metadata(mixed $metadata): array
|
||||
{
|
||||
if (is_array($metadata)) {
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
if (!is_string($metadata) || trim($metadata) === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
$decoded = json_decode($metadata, true, 512, JSON_THROW_ON_ERROR);
|
||||
return is_array($decoded) ? $decoded : [];
|
||||
} catch (JsonException) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private function jsonResponse(array $data, int $status): Response
|
||||
{
|
||||
return response(
|
||||
json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR),
|
||||
$status,
|
||||
['Content-Type' => 'application/json']
|
||||
);
|
||||
}
|
||||
}
|
||||
42
app/controller/IndexController.php
Normal file
42
app/controller/IndexController.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace app\controller;
|
||||
|
||||
use support\Request;
|
||||
|
||||
class IndexController
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
return <<<EOF
|
||||
<style>
|
||||
* {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
iframe {
|
||||
border: none;
|
||||
overflow: scroll;
|
||||
}
|
||||
</style>
|
||||
<iframe
|
||||
src="https://www.workerman.net/wellcome"
|
||||
width="100%"
|
||||
height="100%"
|
||||
allow="clipboard-write"
|
||||
sandbox="allow-scripts allow-same-origin allow-popups allow-downloads"
|
||||
></iframe>
|
||||
EOF;
|
||||
}
|
||||
|
||||
public function view(Request $request)
|
||||
{
|
||||
return view('index/view', ['name' => 'webman']);
|
||||
}
|
||||
|
||||
public function json(Request $request)
|
||||
{
|
||||
return json(['code' => 0, 'msg' => 'ok']);
|
||||
}
|
||||
|
||||
}
|
||||
4
app/functions.php
Normal file
4
app/functions.php
Normal file
@ -0,0 +1,4 @@
|
||||
<?php
|
||||
/**
|
||||
* Here is your custom functions.
|
||||
*/
|
||||
42
app/middleware/StaticFile.php
Normal file
42
app/middleware/StaticFile.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
namespace app\middleware;
|
||||
|
||||
use Webman\MiddlewareInterface;
|
||||
use Webman\Http\Response;
|
||||
use Webman\Http\Request;
|
||||
|
||||
/**
|
||||
* Class StaticFile
|
||||
* @package app\middleware
|
||||
*/
|
||||
class StaticFile implements MiddlewareInterface
|
||||
{
|
||||
public function process(Request $request, callable $handler): Response
|
||||
{
|
||||
// Access to files beginning with. Is prohibited
|
||||
if (strpos($request->path(), '/.') !== false) {
|
||||
return response('<h1>403 forbidden</h1>', 403);
|
||||
}
|
||||
/** @var Response $response */
|
||||
$response = $handler($request);
|
||||
// Add cross domain HTTP header
|
||||
/*$response->withHeaders([
|
||||
'Access-Control-Allow-Origin' => '*',
|
||||
'Access-Control-Allow-Credentials' => 'true',
|
||||
]);*/
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
29
app/model/Test.php
Normal file
29
app/model/Test.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class Test extends Model
|
||||
{
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'test';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = false;
|
||||
}
|
||||
77
app/process/AiMetadata.php
Normal file
77
app/process/AiMetadata.php
Normal file
@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace app\process;
|
||||
|
||||
use app\service\AiMetadataQueue;
|
||||
use app\service\ArchiveMetadataEnrichmentService;
|
||||
use app\service\ArchiveRepository;
|
||||
use Throwable;
|
||||
use Workerman\Timer;
|
||||
|
||||
class AiMetadata
|
||||
{
|
||||
private AiMetadataQueue $queue;
|
||||
private ArchiveRepository $archives;
|
||||
private ArchiveMetadataEnrichmentService $enrichment;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->queue = new AiMetadataQueue();
|
||||
$this->archives = new ArchiveRepository();
|
||||
$this->enrichment = new ArchiveMetadataEnrichmentService();
|
||||
}
|
||||
|
||||
public function onWorkerStart(): void
|
||||
{
|
||||
Timer::add(10, fn (): int => $this->queue->releaseDueDelayed());
|
||||
|
||||
while (true) {
|
||||
$this->queue->releaseDueDelayed();
|
||||
$archiveUid = $this->queue->pop($this->queue->blockTimeout());
|
||||
if ($archiveUid === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->handle($archiveUid);
|
||||
}
|
||||
}
|
||||
|
||||
private function handle(string $archiveUid): void
|
||||
{
|
||||
try {
|
||||
$archive = $this->archives->findArchive($archiveUid);
|
||||
if ($archive === null) {
|
||||
$this->queue->clearRetry($archiveUid);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->archives->archiveNeedsMetadata($archive)) {
|
||||
$this->queue->clearRetry($archiveUid);
|
||||
return;
|
||||
}
|
||||
|
||||
$payload = $archive;
|
||||
$payload['content'] = $this->archives->findChunksText($archiveUid);
|
||||
|
||||
$enriched = $this->enrichment->enrich($payload);
|
||||
$aiMeta = $enriched['metadata']['ai_enrichment'] ?? [];
|
||||
|
||||
if (($aiMeta['attempted'] ?? false) !== true || ($aiMeta['error'] ?? null)) {
|
||||
$this->queue->retryLater($archiveUid, $aiMeta['error'] ?? 'AI metadata enrichment did not complete.');
|
||||
return;
|
||||
}
|
||||
|
||||
$fields = [];
|
||||
foreach (['title', 'year', 'author', 'tags', 'summary'] as $field) {
|
||||
if (array_key_exists($field, $enriched)) {
|
||||
$fields[$field] = $enriched[$field];
|
||||
}
|
||||
}
|
||||
|
||||
$this->archives->updateMetadata($archiveUid, $fields, $aiMeta);
|
||||
$this->queue->clearRetry($archiveUid);
|
||||
} catch (Throwable $exception) {
|
||||
$this->queue->retryLater($archiveUid, $exception->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
10
app/process/Http.php
Normal file
10
app/process/Http.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace app\process;
|
||||
|
||||
use Webman\App;
|
||||
|
||||
class Http extends App
|
||||
{
|
||||
|
||||
}
|
||||
305
app/process/Monitor.php
Normal file
305
app/process/Monitor.php
Normal file
@ -0,0 +1,305 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
namespace app\process;
|
||||
|
||||
use FilesystemIterator;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use SplFileInfo;
|
||||
use Workerman\Timer;
|
||||
use Workerman\Worker;
|
||||
|
||||
/**
|
||||
* Class FileMonitor
|
||||
* @package process
|
||||
*/
|
||||
class Monitor
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $paths = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $extensions = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected array $loadedFiles = [];
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected int $ppid = 0;
|
||||
|
||||
/**
|
||||
* Pause monitor
|
||||
* @return void
|
||||
*/
|
||||
public static function pause(): void
|
||||
{
|
||||
file_put_contents(static::lockFile(), time());
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume monitor
|
||||
* @return void
|
||||
*/
|
||||
public static function resume(): void
|
||||
{
|
||||
clearstatcache();
|
||||
if (is_file(static::lockFile())) {
|
||||
unlink(static::lockFile());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether monitor is paused
|
||||
* @return bool
|
||||
*/
|
||||
public static function isPaused(): bool
|
||||
{
|
||||
clearstatcache();
|
||||
return file_exists(static::lockFile());
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock file
|
||||
* @return string
|
||||
*/
|
||||
protected static function lockFile(): string
|
||||
{
|
||||
return runtime_path('monitor.lock');
|
||||
}
|
||||
|
||||
/**
|
||||
* FileMonitor constructor.
|
||||
* @param $monitorDir
|
||||
* @param $monitorExtensions
|
||||
* @param array $options
|
||||
*/
|
||||
public function __construct($monitorDir, $monitorExtensions, array $options = [])
|
||||
{
|
||||
$this->ppid = function_exists('posix_getppid') ? posix_getppid() : 0;
|
||||
static::resume();
|
||||
$this->paths = (array)$monitorDir;
|
||||
$this->extensions = $monitorExtensions;
|
||||
foreach (get_included_files() as $index => $file) {
|
||||
$this->loadedFiles[$file] = $index;
|
||||
if (strpos($file, 'webman-framework/src/support/App.php')) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!Worker::getAllWorkers()) {
|
||||
return;
|
||||
}
|
||||
$disableFunctions = explode(',', ini_get('disable_functions'));
|
||||
if (in_array('exec', $disableFunctions, true)) {
|
||||
echo "\nMonitor file change turned off because exec() has been disabled by disable_functions setting in " . PHP_CONFIG_FILE_PATH . "/php.ini\n";
|
||||
} else {
|
||||
if ($options['enable_file_monitor'] ?? true) {
|
||||
Timer::add(1, function () {
|
||||
$this->checkAllFilesChange();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$memoryLimit = $this->getMemoryLimit($options['memory_limit'] ?? null);
|
||||
if ($memoryLimit && ($options['enable_memory_monitor'] ?? true)) {
|
||||
Timer::add(60, [$this, 'checkMemory'], [$memoryLimit]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $monitorDir
|
||||
* @return bool
|
||||
*/
|
||||
public function checkFilesChange($monitorDir): bool
|
||||
{
|
||||
static $lastMtime, $tooManyFilesCheck;
|
||||
if (!$lastMtime) {
|
||||
$lastMtime = time();
|
||||
}
|
||||
clearstatcache();
|
||||
if (!is_dir($monitorDir)) {
|
||||
if (!is_file($monitorDir)) {
|
||||
return false;
|
||||
}
|
||||
$iterator = [new SplFileInfo($monitorDir)];
|
||||
} else {
|
||||
// recursive traversal directory
|
||||
$dirIterator = new RecursiveDirectoryIterator($monitorDir, FilesystemIterator::SKIP_DOTS | FilesystemIterator::FOLLOW_SYMLINKS);
|
||||
$iterator = new RecursiveIteratorIterator($dirIterator);
|
||||
}
|
||||
$count = 0;
|
||||
foreach ($iterator as $file) {
|
||||
$count ++;
|
||||
/** @var SplFileInfo $file */
|
||||
if (is_dir($file->getRealPath())) {
|
||||
continue;
|
||||
}
|
||||
// check mtime
|
||||
if (in_array($file->getExtension(), $this->extensions, true) && $lastMtime < $file->getMTime()) {
|
||||
$lastMtime = $file->getMTime();
|
||||
if (DIRECTORY_SEPARATOR === '/' && isset($this->loadedFiles[$file->getRealPath()])) {
|
||||
echo "$file updated but cannot be reloaded because only auto-loaded files support reload.\n";
|
||||
continue;
|
||||
}
|
||||
$var = 0;
|
||||
exec('"'.PHP_BINARY . '" -l ' . $file, $out, $var);
|
||||
if ($var) {
|
||||
continue;
|
||||
}
|
||||
// send SIGUSR1 signal to master process for reload
|
||||
if (DIRECTORY_SEPARATOR === '/') {
|
||||
if ($masterPid = $this->getMasterPid()) {
|
||||
echo $file . " updated and reload\n";
|
||||
posix_kill($masterPid, SIGUSR1);
|
||||
} else {
|
||||
echo "Master process has gone away and can not reload\n";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
echo $file . " updated and reload\n";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (!$tooManyFilesCheck && $count > 1000) {
|
||||
echo "Monitor: There are too many files ($count files) in $monitorDir which makes file monitoring very slow\n";
|
||||
$tooManyFilesCheck = 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getMasterPid(): int
|
||||
{
|
||||
if ($this->ppid === 0) {
|
||||
return 0;
|
||||
}
|
||||
if (function_exists('posix_kill') && !posix_kill($this->ppid, 0)) {
|
||||
echo "Master process has gone away\n";
|
||||
return $this->ppid = 0;
|
||||
}
|
||||
if (PHP_OS_FAMILY !== 'Linux') {
|
||||
return $this->ppid;
|
||||
}
|
||||
$cmdline = "/proc/$this->ppid/cmdline";
|
||||
if (!is_readable($cmdline) || !($content = file_get_contents($cmdline)) || (!str_contains($content, 'WorkerMan') && !str_contains($content, 'php'))) {
|
||||
// Process not exist
|
||||
$this->ppid = 0;
|
||||
}
|
||||
return $this->ppid;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function checkAllFilesChange(): bool
|
||||
{
|
||||
if (static::isPaused()) {
|
||||
return false;
|
||||
}
|
||||
foreach ($this->paths as $path) {
|
||||
if ($this->checkFilesChange($path)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $memoryLimit
|
||||
* @return void
|
||||
*/
|
||||
public function checkMemory($memoryLimit): void
|
||||
{
|
||||
if (static::isPaused() || $memoryLimit <= 0) {
|
||||
return;
|
||||
}
|
||||
$masterPid = $this->getMasterPid();
|
||||
if ($masterPid <= 0) {
|
||||
echo "Master process has gone away\n";
|
||||
return;
|
||||
}
|
||||
|
||||
$childrenFile = "/proc/$masterPid/task/$masterPid/children";
|
||||
if (!is_file($childrenFile) || !($children = file_get_contents($childrenFile))) {
|
||||
return;
|
||||
}
|
||||
foreach (explode(' ', $children) as $pid) {
|
||||
$pid = (int)$pid;
|
||||
$statusFile = "/proc/$pid/status";
|
||||
if (!is_file($statusFile) || !($status = file_get_contents($statusFile))) {
|
||||
continue;
|
||||
}
|
||||
$mem = 0;
|
||||
if (preg_match('/VmRSS\s*?:\s*?(\d+?)\s*?kB/', $status, $match)) {
|
||||
$mem = $match[1];
|
||||
}
|
||||
$mem = (int)($mem / 1024);
|
||||
if ($mem >= $memoryLimit) {
|
||||
posix_kill($pid, SIGINT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get memory limit
|
||||
* @param $memoryLimit
|
||||
* @return int
|
||||
*/
|
||||
protected function getMemoryLimit($memoryLimit): int
|
||||
{
|
||||
if ($memoryLimit === 0) {
|
||||
return 0;
|
||||
}
|
||||
$usePhpIni = false;
|
||||
if (!$memoryLimit) {
|
||||
$memoryLimit = ini_get('memory_limit');
|
||||
$usePhpIni = true;
|
||||
}
|
||||
|
||||
if ($memoryLimit == -1) {
|
||||
return 0;
|
||||
}
|
||||
$unit = strtolower($memoryLimit[strlen($memoryLimit) - 1]);
|
||||
$memoryLimit = (int)$memoryLimit;
|
||||
if ($unit === 'g') {
|
||||
$memoryLimit = 1024 * $memoryLimit;
|
||||
} else if ($unit === 'k') {
|
||||
$memoryLimit = ($memoryLimit / 1024);
|
||||
} else if ($unit === 'm') {
|
||||
$memoryLimit = (int)($memoryLimit);
|
||||
} else if ($unit === 't') {
|
||||
$memoryLimit = (1024 * 1024 * $memoryLimit);
|
||||
} else {
|
||||
$memoryLimit = ($memoryLimit / (1024 * 1024));
|
||||
}
|
||||
if ($memoryLimit < 50) {
|
||||
$memoryLimit = 50;
|
||||
}
|
||||
if ($usePhpIni) {
|
||||
$memoryLimit = (0.8 * $memoryLimit);
|
||||
}
|
||||
return (int)$memoryLimit;
|
||||
}
|
||||
|
||||
}
|
||||
99
app/service/AiMetadataQueue.php
Normal file
99
app/service/AiMetadataQueue.php
Normal file
@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace app\service;
|
||||
|
||||
use support\Redis;
|
||||
|
||||
class AiMetadataQueue
|
||||
{
|
||||
public function push(string $archiveUid): void
|
||||
{
|
||||
Redis::lPush($this->pendingKey(), $archiveUid);
|
||||
}
|
||||
|
||||
public function pop(int $timeout = 5): ?string
|
||||
{
|
||||
$result = Redis::brPop([$this->pendingKey()], $timeout);
|
||||
if (!is_array($result) || count($result) < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (string) $result[1];
|
||||
}
|
||||
|
||||
public function retryLater(string $archiveUid, string $error): void
|
||||
{
|
||||
$retryKey = $this->retryKey($archiveUid);
|
||||
$retryCount = (int) Redis::incr($retryKey);
|
||||
Redis::setEx($this->errorKey($archiveUid), 86400, $error);
|
||||
|
||||
if ($retryCount > $this->maxRetries()) {
|
||||
Redis::lPush($this->failedKey(), $archiveUid);
|
||||
return;
|
||||
}
|
||||
|
||||
$delay = $this->baseDelaySeconds() * (2 ** max(0, $retryCount - 1));
|
||||
Redis::zAdd($this->delayedKey(), time() + $delay, $archiveUid);
|
||||
}
|
||||
|
||||
public function releaseDueDelayed(): int
|
||||
{
|
||||
$now = time();
|
||||
$items = Redis::zRangeByScore($this->delayedKey(), '-inf', (string) $now, ['limit' => [0, 100]]);
|
||||
if (!is_array($items) || $items === []) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
foreach ($items as $archiveUid) {
|
||||
Redis::zRem($this->delayedKey(), $archiveUid);
|
||||
Redis::lPush($this->pendingKey(), $archiveUid);
|
||||
}
|
||||
|
||||
return count($items);
|
||||
}
|
||||
|
||||
public function clearRetry(string $archiveUid): void
|
||||
{
|
||||
Redis::del($this->retryKey($archiveUid), $this->errorKey($archiveUid));
|
||||
}
|
||||
|
||||
public function blockTimeout(): int
|
||||
{
|
||||
return (int) config('queue.ai_metadata.block_timeout', 5);
|
||||
}
|
||||
|
||||
private function pendingKey(): string
|
||||
{
|
||||
return config('queue.ai_metadata.pending', 'proofdb:ai:metadata:pending');
|
||||
}
|
||||
|
||||
private function delayedKey(): string
|
||||
{
|
||||
return config('queue.ai_metadata.delayed', 'proofdb:ai:metadata:delayed');
|
||||
}
|
||||
|
||||
private function failedKey(): string
|
||||
{
|
||||
return config('queue.ai_metadata.failed', 'proofdb:ai:metadata:failed');
|
||||
}
|
||||
|
||||
private function retryKey(string $archiveUid): string
|
||||
{
|
||||
return config('queue.ai_metadata.retry_prefix', 'proofdb:ai:metadata:retry:') . $archiveUid;
|
||||
}
|
||||
|
||||
private function errorKey(string $archiveUid): string
|
||||
{
|
||||
return config('queue.ai_metadata.error_prefix', 'proofdb:ai:metadata:error:') . $archiveUid;
|
||||
}
|
||||
|
||||
private function maxRetries(): int
|
||||
{
|
||||
return (int) config('queue.ai_metadata.max_retries', 5);
|
||||
}
|
||||
|
||||
private function baseDelaySeconds(): int
|
||||
{
|
||||
return (int) config('queue.ai_metadata.base_delay_seconds', 60);
|
||||
}
|
||||
}
|
||||
197
app/service/ArchiveMetadataEnrichmentService.php
Normal file
197
app/service/ArchiveMetadataEnrichmentService.php
Normal file
@ -0,0 +1,197 @@
|
||||
<?php
|
||||
|
||||
namespace app\service;
|
||||
|
||||
use app\service\LLM\OpenAICompatibleClient;
|
||||
use app\service\LLM\LLMRetryQueue;
|
||||
use Throwable;
|
||||
|
||||
class ArchiveMetadataEnrichmentService
|
||||
{
|
||||
private OpenAICompatibleClient $client;
|
||||
private LLMRetryQueue $queue;
|
||||
|
||||
public function __construct(?OpenAICompatibleClient $client = null, ?LLMRetryQueue $queue = null)
|
||||
{
|
||||
$this->client = $client ?? new OpenAICompatibleClient();
|
||||
$this->queue = $queue ?? new LLMRetryQueue();
|
||||
}
|
||||
|
||||
public function enrich(array $payload): array
|
||||
{
|
||||
$missing = $this->missingFields($payload);
|
||||
if ($missing === [] || !$this->enabled()) {
|
||||
return $this->withAiMeta($payload, [
|
||||
'enabled' => $this->enabled(),
|
||||
'attempted' => false,
|
||||
'filled' => [],
|
||||
'missing' => $missing,
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $this->queue->run(
|
||||
fn (): array => $this->client->chatJson($this->messages($payload, $missing), [
|
||||
'model' => config('LLMapi.metadata.model'),
|
||||
'temperature' => config('LLMapi.metadata.temperature', 0.1),
|
||||
'max_tokens' => config('LLMapi.metadata.max_tokens', 1200),
|
||||
'stream' => false,
|
||||
'response_format' => config('LLMapi.metadata.response_format', ['type' => 'json_object']),
|
||||
'thinking' => config('LLMapi.metadata.thinking', ['type' => 'disabled']),
|
||||
'request_id' => $this->requestId($payload, $missing),
|
||||
]),
|
||||
config('LLMapi.metadata.retry', [])
|
||||
);
|
||||
} catch (Throwable $exception) {
|
||||
return $this->withAiMeta($payload, [
|
||||
'enabled' => true,
|
||||
'attempted' => true,
|
||||
'filled' => [],
|
||||
'missing' => $missing,
|
||||
'error' => $exception->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
$filled = [];
|
||||
foreach ($missing as $field) {
|
||||
if (!$this->hasUsefulValue($result, $field)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$payload[$field] = $this->normalizeField($field, $result[$field]);
|
||||
$filled[] = $field;
|
||||
}
|
||||
|
||||
return $this->withAiMeta($payload, [
|
||||
'enabled' => true,
|
||||
'attempted' => true,
|
||||
'filled' => $filled,
|
||||
'missing' => array_values(array_diff($missing, $filled)),
|
||||
'model' => config('LLMapi.metadata.model'),
|
||||
'stream' => false,
|
||||
'response_format' => config('LLMapi.metadata.response_format', ['type' => 'json_object']),
|
||||
'thinking' => config('LLMapi.metadata.thinking', ['type' => 'disabled']),
|
||||
]);
|
||||
}
|
||||
|
||||
private function missingFields(array $payload): array
|
||||
{
|
||||
$fields = ['title', 'year', 'author', 'tags', 'summary'];
|
||||
return array_values(array_filter($fields, fn (string $field): bool => !$this->hasUsefulValue($payload, $field)));
|
||||
}
|
||||
|
||||
private function enabled(): bool
|
||||
{
|
||||
return (bool) config('LLMapi.metadata.enabled', true) && $this->client->isConfigured();
|
||||
}
|
||||
|
||||
private function messages(array $payload, array $missing): array
|
||||
{
|
||||
$text = $this->sampleText($payload);
|
||||
|
||||
return [
|
||||
[
|
||||
'role' => 'system',
|
||||
'content' => implode("\n", [
|
||||
'你是历史档案元数据整理助手。',
|
||||
'你只能根据用户提供的档案文本抽取或推断元数据。',
|
||||
'请只返回 JSON 对象,不要返回 Markdown,不要解释。',
|
||||
'字段:title(string), year(integer|null), author(string|null), tags(array<string>), summary(string)。',
|
||||
'summary 简洁概括档案内容,80-200 字。',
|
||||
'tags 用档案中常见专名和涉及主题,5-10 个。',
|
||||
'无法判断的字段返回 null 或空数组。',
|
||||
'以上请均使用档案中的语言。',
|
||||
]),
|
||||
],
|
||||
[
|
||||
'role' => 'user',
|
||||
'content' => json_encode([
|
||||
'missing_fields' => $missing,
|
||||
'known_fields' => [
|
||||
'title' => $payload['title'] ?? null,
|
||||
'year' => $payload['year'] ?? null,
|
||||
'author' => $payload['author'] ?? null,
|
||||
'source' => $payload['source'] ?? null,
|
||||
'series' => $payload['series'] ?? null,
|
||||
],
|
||||
'archive_text_sample' => $text,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function requestId(array $payload, array $missing): string
|
||||
{
|
||||
return 'metadata-' . substr(hash('sha256', implode('|', [
|
||||
(string) ($payload['source'] ?? ''),
|
||||
(string) ($payload['archive_uid'] ?? ''),
|
||||
mb_substr((string) ($payload['content'] ?? ''), 0, 1000),
|
||||
implode(',', $missing ?? []),
|
||||
])), 0, 32);
|
||||
}
|
||||
|
||||
private function sampleText(array $payload): string
|
||||
{
|
||||
$text = '';
|
||||
if (isset($payload['content']) && is_string($payload['content'])) {
|
||||
$text = $payload['content'];
|
||||
} elseif (isset($payload['pages']) && is_array($payload['pages'])) {
|
||||
$parts = [];
|
||||
foreach ($payload['pages'] as $page) {
|
||||
if (isset($page['content']) && is_string($page['content'])) {
|
||||
$parts[] = $page['content'];
|
||||
}
|
||||
}
|
||||
$text = implode("\n\n", $parts);
|
||||
}
|
||||
|
||||
$maxChars = (int) config('LLMapi.metadata.max_input_chars', 12000);
|
||||
return mb_substr($text, 0, $maxChars);
|
||||
}
|
||||
|
||||
private function hasUsefulValue(array $payload, string $field): bool
|
||||
{
|
||||
if (!array_key_exists($field, $payload)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($field === 'title' && (($payload['metadata']['title_source'] ?? null) === 'fallback')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$value = $payload[$field];
|
||||
if (is_array($value)) {
|
||||
return $value !== [];
|
||||
}
|
||||
|
||||
if ($field === 'year') {
|
||||
return is_numeric($value) && (int) $value > 0;
|
||||
}
|
||||
|
||||
return is_string($value) ? trim($value) !== '' : $value !== null;
|
||||
}
|
||||
|
||||
private function normalizeField(string $field, mixed $value): mixed
|
||||
{
|
||||
if ($field === 'year') {
|
||||
return is_numeric($value) ? (int) $value : null;
|
||||
}
|
||||
|
||||
if ($field === 'tags') {
|
||||
if (!is_array($value)) {
|
||||
return [];
|
||||
}
|
||||
return array_values(array_filter(array_map('strval', $value)));
|
||||
}
|
||||
|
||||
return is_string($value) ? trim($value) : $value;
|
||||
}
|
||||
|
||||
private function withAiMeta(array $payload, array $ai): array
|
||||
{
|
||||
$payload['metadata'] = is_array($payload['metadata'] ?? null) ? $payload['metadata'] : [];
|
||||
$payload['metadata']['ai_enrichment'] = $ai;
|
||||
|
||||
return $payload;
|
||||
}
|
||||
}
|
||||
134
app/service/ArchiveRepository.php
Normal file
134
app/service/ArchiveRepository.php
Normal file
@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace app\service;
|
||||
|
||||
use support\Db;
|
||||
|
||||
class ArchiveRepository
|
||||
{
|
||||
public function saveImport(array $import): void
|
||||
{
|
||||
Db::transaction(function () use ($import): void {
|
||||
$archive = $import['archive'];
|
||||
$chunks = $import['chunks'];
|
||||
$chunkUids = array_column($chunks, 'chunk_uid');
|
||||
|
||||
Db::table('archives')->updateOrInsert(
|
||||
['archive_uid' => $archive['archive_uid']],
|
||||
[
|
||||
'title' => $archive['title'] ?? null,
|
||||
'summary' => $archive['summary'] ?? null,
|
||||
'year' => $archive['year'] ?? null,
|
||||
'author' => $archive['author'] ?? null,
|
||||
'source' => $archive['source'] ?? null,
|
||||
'series' => $archive['series'] ?? null,
|
||||
'tags' => json_encode($archive['tags'] ?? [], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
'metadata' => json_encode($archive['metadata'] ?? [], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
'content' => $archive['content'] ?? null,
|
||||
'raw' => $archive['raw'] ?? null,
|
||||
'chunks' => json_encode($chunkUids, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
]
|
||||
);
|
||||
|
||||
Db::table('chunks')->where('archive_uid', $archive['archive_uid'])->delete();
|
||||
foreach ($chunks as $chunk) {
|
||||
Db::table('chunks')->insert([
|
||||
'chunk_uid' => $chunk['chunk_uid'],
|
||||
'archive_uid' => $archive['archive_uid'],
|
||||
'chunk_index' => $chunk['chunk_index'],
|
||||
'page_start' => $chunk['page_start'],
|
||||
'page_end' => $chunk['page_end'],
|
||||
'text' => $chunk['text'],
|
||||
'length' => $chunk['length'],
|
||||
'embedding_status' => 0,
|
||||
'embedding_ref' => null,
|
||||
'embedding_model' => null,
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function findArchive(string $archiveUid): ?array
|
||||
{
|
||||
$archive = Db::table('archives')->where('archive_uid', $archiveUid)->first();
|
||||
if (!$archive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->archiveToArray($archive);
|
||||
}
|
||||
|
||||
public function findChunksText(string $archiveUid, int $limit = 20): string
|
||||
{
|
||||
$chunks = Db::table('chunks')
|
||||
->where('archive_uid', $archiveUid)
|
||||
->orderBy('chunk_index')
|
||||
->limit($limit)
|
||||
->get(['text'])
|
||||
->all();
|
||||
|
||||
return implode("\n\n", array_map(fn ($chunk): string => (string) $chunk->text, $chunks));
|
||||
}
|
||||
|
||||
public function updateMetadata(string $archiveUid, array $fields, array $aiMeta): void
|
||||
{
|
||||
$archive = $this->findArchive($archiveUid);
|
||||
$metadata = $archive['metadata'] ?? [];
|
||||
$metadata['ai_enrichment'] = $aiMeta;
|
||||
|
||||
$updates = [
|
||||
'metadata' => json_encode($metadata, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
];
|
||||
|
||||
foreach (['title', 'summary', 'year', 'author', 'series', 'tags'] as $field) {
|
||||
if (!array_key_exists($field, $fields)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$updates[$field] = $field === 'tags'
|
||||
? json_encode($fields[$field] ?? [], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
|
||||
: $fields[$field];
|
||||
}
|
||||
|
||||
Db::table('archives')->where('archive_uid', $archiveUid)->update($updates);
|
||||
}
|
||||
|
||||
public function archiveNeedsMetadata(array $archive): bool
|
||||
{
|
||||
foreach (['title', 'year', 'author', 'tags', 'summary'] as $field) {
|
||||
$value = $archive[$field] ?? null;
|
||||
if ($field === 'title' && (($archive['metadata']['title_source'] ?? null) === 'fallback')) {
|
||||
return true;
|
||||
}
|
||||
if (is_array($value) && $value === []) {
|
||||
return true;
|
||||
}
|
||||
if ($field === 'year' && (!$value || (int) $value <= 0)) {
|
||||
return true;
|
||||
}
|
||||
if (!is_array($value) && ($value === null || trim((string) $value) === '')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function archiveToArray(object $archive): array
|
||||
{
|
||||
return [
|
||||
'archive_uid' => $archive->archive_uid,
|
||||
'title' => $archive->title,
|
||||
'summary' => $archive->summary,
|
||||
'year' => $archive->year,
|
||||
'author' => $archive->author,
|
||||
'source' => $archive->source,
|
||||
'series' => $archive->series,
|
||||
'tags' => json_decode($archive->tags ?? '[]', true) ?: [],
|
||||
'metadata' => json_decode($archive->metadata ?? '{}', true) ?: [],
|
||||
'content' => $archive->content,
|
||||
'raw' => $archive->raw,
|
||||
'chunks' => json_decode($archive->chunks ?? '[]', true) ?: [],
|
||||
];
|
||||
}
|
||||
}
|
||||
775
app/service/ArticleImportService.php
Normal file
775
app/service/ArticleImportService.php
Normal file
@ -0,0 +1,775 @@
|
||||
<?php
|
||||
|
||||
namespace app\service;
|
||||
|
||||
use RuntimeException;
|
||||
use Symfony\Component\Uid\Ulid;
|
||||
|
||||
class ArticleImportService
|
||||
{
|
||||
private const DEFAULT_CHUNK_SIZE = 800;
|
||||
private const DEFAULT_CHUNK_OVERLAP = 120;
|
||||
private const MIN_CHUNK_SIZE = 100;
|
||||
private const MAX_CHUNK_SIZE = 4000;
|
||||
private const SENTENCE_BOUNDARY_PATTERN = '/(?<=[。!?;.!?;])\s*|\R+/u';
|
||||
private const MARKDOWN_PAGE_PATTERN = '/<!--\s*DOCMASTER:PAGE\s+0*([0-9A-Za-z_-]+)\s*-->\s*(?:#+\s*Page\s+\S+\s*)?(.*?)(?=\n---\s*\n\s*<!--\s*DOCMASTER:PAGE|\z)/su';
|
||||
|
||||
public function import(array $payload): array
|
||||
{
|
||||
$payload = $this->normalizePayload($payload);
|
||||
$payload = $this->applyMetadataFallbacks($payload);
|
||||
$errors = $this->validate($payload);
|
||||
if ($errors !== []) {
|
||||
return ['ok' => false, 'errors' => $errors];
|
||||
}
|
||||
|
||||
$archiveUid = $this->archiveUid($payload);
|
||||
$archive = $this->archive($payload, $archiveUid);
|
||||
$pageBlocks = $this->pageBlocks($payload);
|
||||
$chunkSize = $this->intOption($payload, 'chunk_size', self::DEFAULT_CHUNK_SIZE);
|
||||
$chunkOverlap = $this->intOption($payload, 'chunk_overlap', self::DEFAULT_CHUNK_OVERLAP);
|
||||
|
||||
$chunks = $this->chunksFromPages($archiveUid, $pageBlocks, $chunkSize, $chunkOverlap);
|
||||
$pages = $this->pagesSummary($pageBlocks, $chunks);
|
||||
$needsAiMetadata = (new ArchiveRepository())->archiveNeedsMetadata($archive);
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'data' => [
|
||||
'import_uid' => $archiveUid,
|
||||
'archive' => $archive,
|
||||
'chunks' => $chunks,
|
||||
'pages' => $pages,
|
||||
'stats' => [
|
||||
'page_count' => count($pages),
|
||||
'page_block_count' => count($pageBlocks),
|
||||
'chunk_count' => count($chunks),
|
||||
'chunk_size' => $chunkSize,
|
||||
'chunk_overlap' => $chunkOverlap,
|
||||
],
|
||||
'queue' => [
|
||||
'ai_metadata_enqueued' => false,
|
||||
'needs_ai_metadata' => $needsAiMetadata,
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function persistSnapshot(array $import): void
|
||||
{
|
||||
$directory = runtime_path('proofdb/imports');
|
||||
if (!is_dir($directory) && !mkdir($directory, 0775, true) && !is_dir($directory)) {
|
||||
throw new RuntimeException("Unable to create directory: {$directory}");
|
||||
}
|
||||
|
||||
$path = $directory . DIRECTORY_SEPARATOR . $import['import_uid'] . '.json';
|
||||
$json = json_encode($import, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
|
||||
|
||||
if (file_put_contents($path, $json) === false) {
|
||||
throw new RuntimeException("Unable to write import snapshot: {$path}");
|
||||
}
|
||||
}
|
||||
|
||||
private function validate(array $payload): array
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
if (isset($payload['file_error'])) {
|
||||
$errors['file'][] = 'uploaded file is invalid.';
|
||||
}
|
||||
|
||||
if (!isset($payload['title']) || !is_string($payload['title']) || trim($payload['title']) === '') {
|
||||
$errors['title'][] = 'title is required.';
|
||||
}
|
||||
|
||||
if (!isset($payload['source']) || !is_string($payload['source']) || trim($payload['source']) === '') {
|
||||
$errors['source'][] = 'source is required.';
|
||||
}
|
||||
|
||||
if (!isset($payload['content']) && !isset($payload['paragraphs']) && !isset($payload['pages'])) {
|
||||
$errors['content'][] = 'content, file, pages, or paragraphs is required.';
|
||||
}
|
||||
|
||||
if (isset($payload['paragraphs']) && !is_array($payload['paragraphs'])) {
|
||||
$errors['paragraphs'][] = 'paragraphs must be an array.';
|
||||
}
|
||||
|
||||
if (isset($payload['pages']) && !is_array($payload['pages'])) {
|
||||
$errors['pages'][] = 'pages must be an array.';
|
||||
}
|
||||
|
||||
if (isset($payload['metadata']) && !is_array($payload['metadata'])) {
|
||||
$errors['metadata'][] = 'metadata must be an object.';
|
||||
}
|
||||
|
||||
$chunkSize = $this->intOption($payload, 'chunk_size', self::DEFAULT_CHUNK_SIZE);
|
||||
if ($chunkSize < self::MIN_CHUNK_SIZE || $chunkSize > self::MAX_CHUNK_SIZE) {
|
||||
$errors['chunk_size'][] = 'chunk_size must be between 100 and 4000.';
|
||||
}
|
||||
|
||||
$chunkOverlap = $this->intOption($payload, 'chunk_overlap', self::DEFAULT_CHUNK_OVERLAP);
|
||||
if ($chunkOverlap < 0 || $chunkOverlap >= $chunkSize) {
|
||||
$errors['chunk_overlap'][] = 'chunk_overlap must be greater than or equal to 0 and less than chunk_size.';
|
||||
}
|
||||
|
||||
if (!isset($errors['paragraphs']) && isset($payload['paragraphs'])) {
|
||||
$hasContent = false;
|
||||
foreach ($payload['paragraphs'] as $index => $paragraph) {
|
||||
$content = is_array($paragraph) ? ($paragraph['content'] ?? '') : $paragraph;
|
||||
if (!is_string($content)) {
|
||||
$errors["paragraphs.{$index}.content"][] = 'paragraph content must be a string.';
|
||||
continue;
|
||||
}
|
||||
if (trim($content) !== '') {
|
||||
$hasContent = true;
|
||||
}
|
||||
}
|
||||
if (!$hasContent) {
|
||||
$errors['paragraphs'][] = 'paragraphs must contain at least one non-empty paragraph.';
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($errors['pages']) && isset($payload['pages'])) {
|
||||
$hasContent = false;
|
||||
foreach ($payload['pages'] as $index => $page) {
|
||||
if (!is_array($page)) {
|
||||
$errors["pages.{$index}"][] = 'page must be an object.';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$this->hasPageNumber($page)) {
|
||||
$errors["pages.{$index}.page_number"][] = 'page_number is required.';
|
||||
}
|
||||
|
||||
if (!isset($page['content']) || !is_string($page['content'])) {
|
||||
$errors["pages.{$index}.content"][] = 'page content must be a string.';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (trim($page['content']) !== '') {
|
||||
$hasContent = true;
|
||||
}
|
||||
|
||||
if (isset($page['metadata']) && !is_array($page['metadata'])) {
|
||||
$errors["pages.{$index}.metadata"][] = 'page metadata must be an object.';
|
||||
}
|
||||
}
|
||||
|
||||
if (!$hasContent) {
|
||||
$errors['pages'][] = 'pages must contain at least one non-empty page.';
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($payload['content']) && (!is_string($payload['content']) || trim($payload['content']) === '')) {
|
||||
$errors['content'][] = 'content must be a non-empty string.';
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
private function archive(array $payload, string $archiveUid): array
|
||||
{
|
||||
$title = $this->clean($payload['title']);
|
||||
$source = $this->clean($payload['source']);
|
||||
|
||||
return [
|
||||
'archive_uid' => $archiveUid,
|
||||
'title' => $title,
|
||||
'year' => isset($payload['year']) ? (int) $payload['year'] : null,
|
||||
'author' => $this->nullableClean($payload['author'] ?? null),
|
||||
'source' => $source,
|
||||
'series' => $this->nullableClean($payload['series'] ?? null),
|
||||
'tags' => is_array($payload['tags'] ?? null) ? array_values($payload['tags']) : [],
|
||||
'summary' => $this->nullableClean($payload['summary'] ?? null),
|
||||
'metadata' => $payload['metadata'] ?? [],
|
||||
'content' => $this->nullableClean($payload['content_url'] ?? $payload['content_path'] ?? null),
|
||||
'raw' => $this->nullableClean($payload['raw_url'] ?? $payload['raw_path'] ?? null),
|
||||
];
|
||||
}
|
||||
|
||||
private function pageBlocks(array $payload): array
|
||||
{
|
||||
if (isset($payload['pages'])) {
|
||||
return $this->pageBlocksFromPages($payload);
|
||||
}
|
||||
|
||||
if (isset($payload['paragraphs'])) {
|
||||
return $this->pageBlocksFromItems($payload, $payload['paragraphs']);
|
||||
}
|
||||
|
||||
return $this->pageBlocksFromItems($payload, preg_split('/\R{2,}/u', $payload['content']));
|
||||
}
|
||||
|
||||
private function pageBlocksFromPages(array $payload): array
|
||||
{
|
||||
$pageBlocks = [];
|
||||
|
||||
foreach ($payload['pages'] as $pageIndex => $page) {
|
||||
$pageNumber = $this->pageNumber($page);
|
||||
$pageMetadata = $page['metadata'] ?? [];
|
||||
$items = $this->markdownBlocksFromPage($page['content']);
|
||||
|
||||
foreach ($items as $itemIndex => $content) {
|
||||
$pageBlock = $this->pageBlock($payload, $content, count($pageBlocks), $itemIndex, $pageNumber, [
|
||||
'page_index' => $pageIndex,
|
||||
'page_metadata' => $pageMetadata,
|
||||
]);
|
||||
|
||||
if ($pageBlock !== null) {
|
||||
$pageBlocks[] = $pageBlock;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $pageBlocks;
|
||||
}
|
||||
|
||||
private function chunksFromPages(string $archiveUid, array $pageBlocks, int $chunkSize, int $chunkOverlap): array
|
||||
{
|
||||
$chunks = [];
|
||||
$chunkIndex = 1;
|
||||
|
||||
foreach ($this->groupBlocksByPage($pageBlocks) as $pageNumber => $blocks) {
|
||||
$units = [];
|
||||
foreach ($blocks as $block) {
|
||||
$unit = $this->cleanEmbeddingText($block['content']);
|
||||
if ($unit === '' || $this->isNoiseBlock($unit)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$units[] = $unit;
|
||||
}
|
||||
|
||||
foreach ($this->packUnitsForEmbedding($units, $chunkSize, $chunkOverlap) as $text) {
|
||||
$page = $this->restorePageNumber($pageNumber);
|
||||
$chunks[] = $this->chunk($archiveUid, $chunkIndex, $page, $text);
|
||||
$chunkIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
return $chunks;
|
||||
}
|
||||
|
||||
private function groupBlocksByPage(array $pageBlocks): array
|
||||
{
|
||||
$pages = [];
|
||||
foreach ($pageBlocks as $block) {
|
||||
$key = $block['page_number'] === null ? '' : (string) $block['page_number'];
|
||||
$pages[$key][] = $block;
|
||||
}
|
||||
|
||||
return $pages;
|
||||
}
|
||||
|
||||
private function pagesSummary(array $pageBlocks, array $chunks): array
|
||||
{
|
||||
$pages = [];
|
||||
|
||||
foreach ($this->groupBlocksByPage($pageBlocks) as $pageNumber => $blocks) {
|
||||
$page = $this->restorePageNumber($pageNumber);
|
||||
$pageChunks = array_values(array_filter($chunks, fn (array $chunk): bool => $chunk['page_start'] === $page));
|
||||
$contentLength = array_sum(array_map(fn (array $block): int => mb_strlen($block['content']), $blocks));
|
||||
|
||||
$pages[] = [
|
||||
'page_number' => $page,
|
||||
'block_count' => count($blocks),
|
||||
'chunk_count' => count($pageChunks),
|
||||
'content_length' => $contentLength,
|
||||
'chunk_uids' => array_column($pageChunks, 'chunk_uid'),
|
||||
];
|
||||
}
|
||||
|
||||
return $pages;
|
||||
}
|
||||
|
||||
private function packUnitsForEmbedding(array $units, int $chunkSize, int $chunkOverlap): array
|
||||
{
|
||||
$chunks = [];
|
||||
$current = '';
|
||||
|
||||
foreach ($units as $unit) {
|
||||
$unit = $this->clean($unit);
|
||||
if ($unit === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mb_strlen($unit) > $chunkSize) {
|
||||
if ($current !== '') {
|
||||
$chunks[] = $current;
|
||||
$current = '';
|
||||
}
|
||||
|
||||
array_push($chunks, ...$this->chunkLongUnit($unit, $chunkSize, $chunkOverlap));
|
||||
continue;
|
||||
}
|
||||
|
||||
$candidate = $current === '' ? $unit : $current . "\n\n" . $unit;
|
||||
if (mb_strlen($candidate) <= $chunkSize) {
|
||||
$current = $candidate;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($current !== '') {
|
||||
$chunks[] = $current;
|
||||
}
|
||||
$current = $unit;
|
||||
}
|
||||
|
||||
if ($current !== '') {
|
||||
$chunks[] = $current;
|
||||
}
|
||||
|
||||
return $chunks;
|
||||
}
|
||||
|
||||
private function chunk(string $archiveUid, int $chunkIndex, int|string|null $pageNumber, string $text): array
|
||||
{
|
||||
$chunkUid = $this->chunkUid($archiveUid, $chunkIndex, implode('|', [
|
||||
$chunkIndex,
|
||||
(string) ($pageNumber ?? ''),
|
||||
$text,
|
||||
]));
|
||||
|
||||
return [
|
||||
'chunk_uid' => $chunkUid,
|
||||
'chunk_index' => $chunkIndex,
|
||||
'page_start' => $pageNumber,
|
||||
'page_end' => $pageNumber,
|
||||
'pages' => $pageNumber === null ? [] : [$pageNumber],
|
||||
'text' => $text,
|
||||
'length' => mb_strlen($text),
|
||||
'embedding_ref' => null,
|
||||
];
|
||||
}
|
||||
|
||||
private function normalizePayload(array $payload): array
|
||||
{
|
||||
if (isset($payload['content']) && is_string($payload['content']) && !isset($payload['pages']) && !isset($payload['paragraphs'])) {
|
||||
$payload['pages'] = $this->pagesFromMarkdown($payload['content']);
|
||||
}
|
||||
|
||||
if (!isset($payload['source']) || trim((string) $payload['source']) === '') {
|
||||
$payload['source'] = 'raw-markdown';
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
private function applyMetadataFallbacks(array $payload): array
|
||||
{
|
||||
if ((!isset($payload['title']) || trim((string) $payload['title']) === '') && isset($payload['content']) && is_string($payload['content'])) {
|
||||
$payload['title'] = $this->inferTitle($payload['content'], (string) ($payload['source'] ?? ''));
|
||||
$payload['metadata'] = is_array($payload['metadata'] ?? null) ? $payload['metadata'] : [];
|
||||
$payload['metadata']['title_source'] = 'fallback';
|
||||
}
|
||||
|
||||
if (isset($payload['tags']) && is_string($payload['tags'])) {
|
||||
$payload['tags'] = $this->tagsFromString($payload['tags']);
|
||||
}
|
||||
$payload['tags'] = is_array($payload['tags'] ?? null) ? $payload['tags'] : [];
|
||||
|
||||
if (isset($payload['year']) && is_numeric($payload['year'])) {
|
||||
$payload['year'] = (int) $payload['year'];
|
||||
}
|
||||
|
||||
$payload['metadata'] = is_array($payload['metadata'] ?? null) ? $payload['metadata'] : [];
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
private function pagesFromMarkdown(string $markdown): array
|
||||
{
|
||||
preg_match_all(self::MARKDOWN_PAGE_PATTERN, $markdown, $matches, PREG_SET_ORDER);
|
||||
if ($matches === []) {
|
||||
return [[
|
||||
'page_number' => 1,
|
||||
'content' => $this->cleanMarkdownPage($markdown),
|
||||
'metadata' => ['parser' => 'markdown_single_page'],
|
||||
]];
|
||||
}
|
||||
|
||||
$pages = [];
|
||||
foreach ($matches as $index => $match) {
|
||||
$pageNumber = ctype_digit($match[1]) ? (int) $match[1] : $match[1];
|
||||
$pages[] = [
|
||||
'page_number' => $pageNumber,
|
||||
'content' => $this->cleanMarkdownPage($match[2]),
|
||||
'metadata' => [
|
||||
'parser' => 'docmaster_markdown',
|
||||
'page_index' => $index,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return $pages;
|
||||
}
|
||||
|
||||
private function markdownBlocksFromPage(string $content): array
|
||||
{
|
||||
$content = $this->cleanMarkdownPage($content);
|
||||
$blocks = preg_split('/\R{2,}/u', $content, -1, PREG_SPLIT_NO_EMPTY);
|
||||
if ($blocks === false) {
|
||||
return [$content];
|
||||
}
|
||||
|
||||
$records = [];
|
||||
foreach ($blocks as $block) {
|
||||
$block = $this->cleanMarkdownBlock($block);
|
||||
if ($block === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$lastIndex = count($records) - 1;
|
||||
if ($lastIndex >= 0 && $this->isCommentBlock($block) && $this->isPolicyRecordBlock($records[$lastIndex])) {
|
||||
$records[$lastIndex] .= "\n" . $block;
|
||||
continue;
|
||||
}
|
||||
|
||||
$records[] = $block;
|
||||
}
|
||||
|
||||
return $records;
|
||||
}
|
||||
|
||||
private function cleanMarkdownPage(string $content): string
|
||||
{
|
||||
$content = preg_replace('/<!--\s*DOCMASTER:PAGE\s+[^>]+-->/iu', '', $content) ?? $content;
|
||||
$content = preg_replace('/^\s*#+\s*Page\s+\S+\s*$/imu', '', $content) ?? $content;
|
||||
$content = preg_replace('/^\s*---+\s*$/mu', '', $content) ?? $content;
|
||||
$content = html_entity_decode($content, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
|
||||
return trim($content);
|
||||
}
|
||||
|
||||
private function cleanMarkdownBlock(string $block): string
|
||||
{
|
||||
$block = preg_replace('/[ \t]+/u', ' ', $block) ?? $block;
|
||||
$block = preg_replace('/\R[ \t]+/u', "\n", $block) ?? $block;
|
||||
return trim($block);
|
||||
}
|
||||
|
||||
private function isCommentBlock(string $block): bool
|
||||
{
|
||||
return (bool) preg_match('/^\s*(?:[*#\s_~`>-])*COMMENT\b/iu', $this->plainBlock($block));
|
||||
}
|
||||
|
||||
private function isPolicyRecordBlock(string $block): bool
|
||||
{
|
||||
return (bool) preg_match('/^\s*(?:NSAM|NSDM|NSDD|NSD|PD)\s+\d+/iu', $this->plainBlock($block));
|
||||
}
|
||||
|
||||
private function isNoiseBlock(string $block): bool
|
||||
{
|
||||
$plain = strtoupper($this->plainBlock($block));
|
||||
$plain = preg_replace('/\s+/u', ' ', $plain) ?? $plain;
|
||||
|
||||
if ($plain === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (preg_match('/^\d{1,6}$/', $plain)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$patterns = [
|
||||
'/^(?:# )?UNCLASSIFIED$/',
|
||||
'/^TOP SECRET$/',
|
||||
'/^UNCLASSIFIED WITH TOP SECRET ATTACHMENTS$/',
|
||||
'/^DECLASSIFY ON: OADR$/',
|
||||
'/^\*? ?UNCLASSIFIED\*?$/',
|
||||
];
|
||||
|
||||
foreach ($patterns as $pattern) {
|
||||
if (preg_match($pattern, $plain)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function cleanEmbeddingText(string $text): string
|
||||
{
|
||||
$lines = preg_split('/\R/u', $text);
|
||||
if ($lines === false) {
|
||||
return $this->cleanMarkdownBlock($text);
|
||||
}
|
||||
|
||||
$kept = [];
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '' || $this->isNoiseLine($line)) {
|
||||
continue;
|
||||
}
|
||||
$kept[] = $line;
|
||||
}
|
||||
|
||||
return $this->cleanMarkdownBlock(implode("\n", $kept));
|
||||
}
|
||||
|
||||
private function isNoiseLine(string $line): bool
|
||||
{
|
||||
$plain = strtoupper($this->plainBlock($line));
|
||||
$plain = preg_replace('/\s+/u', ' ', $plain) ?? $plain;
|
||||
|
||||
if ($plain === '' || preg_match('/^\d{1,6}$/', $plain)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$patterns = [
|
||||
'/^(?:# )?UNCLASSIFIED$/',
|
||||
'/^TOP SECRET$/',
|
||||
'/^UNCLASSIFIED WITH TOP SECRET ATTACHMENTS$/',
|
||||
'/^DECLASSIFY ON: OADR$/',
|
||||
'/^PARTIALLY DECLASSIFIED\/RELEASED ON .+$/',
|
||||
'/^UNDER PROVISIONS OF .+$/',
|
||||
'/^BY .+ NATIONAL SECURITY COUNCIL$/',
|
||||
'/^F \d{2}-\d+$/',
|
||||
];
|
||||
|
||||
foreach ($patterns as $pattern) {
|
||||
if (preg_match($pattern, $plain)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function plainBlock(string $block): string
|
||||
{
|
||||
$block = str_replace(['*', '_', '`', '~'], '', $block);
|
||||
$block = preg_replace('/\s+/u', ' ', $block) ?? $block;
|
||||
return trim($block);
|
||||
}
|
||||
|
||||
private function inferTitle(string $markdown, string $source): string
|
||||
{
|
||||
if (preg_match_all('/^\s*#+\s*(.+?)\s*$/imu', $markdown, $matches)) {
|
||||
foreach ($matches[1] as $heading) {
|
||||
$heading = $this->clean($heading);
|
||||
if (!preg_match('/^Page\s+\S+$/iu', $heading) && !$this->isNoiseLine($heading)) {
|
||||
return $heading;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($source !== '') {
|
||||
return pathinfo($source, PATHINFO_FILENAME) ?: $source;
|
||||
}
|
||||
|
||||
return 'Untitled Markdown Import';
|
||||
}
|
||||
|
||||
private function pageBlocksFromItems(array $payload, array $items): array
|
||||
{
|
||||
$pageBlocks = [];
|
||||
|
||||
foreach ($items as $index => $item) {
|
||||
$content = is_array($item) ? ($item['content'] ?? '') : $item;
|
||||
$pageNumber = is_array($item) && $this->hasPageNumber($item) ? $this->pageNumber($item) : null;
|
||||
$metadata = is_array($item) ? ($item['metadata'] ?? []) : [];
|
||||
$pageBlock = $this->pageBlock($payload, $content, count($pageBlocks), $index, $pageNumber, $metadata);
|
||||
|
||||
if ($pageBlock !== null) {
|
||||
$pageBlocks[] = $pageBlock;
|
||||
}
|
||||
}
|
||||
|
||||
return $pageBlocks;
|
||||
}
|
||||
|
||||
private function pageBlock(
|
||||
array $payload,
|
||||
mixed $content,
|
||||
int $blockIndex,
|
||||
int $sourceIndex,
|
||||
int|string|null $pageNumber,
|
||||
array $metadata
|
||||
): ?array {
|
||||
$content = $this->clean((string) $content);
|
||||
if ($content === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'block_uid' => $this->uid('block', implode('|', [
|
||||
$payload['source'],
|
||||
$payload['title'],
|
||||
(string) $pageNumber,
|
||||
$sourceIndex,
|
||||
$content,
|
||||
])),
|
||||
'index' => $blockIndex,
|
||||
'page_number' => $pageNumber,
|
||||
'content' => $content,
|
||||
'metadata' => $metadata,
|
||||
];
|
||||
}
|
||||
|
||||
private function chunkLongUnit(string $text, int $chunkSize, int $chunkOverlap): array
|
||||
{
|
||||
if (mb_strlen($text) <= $chunkSize) {
|
||||
return [$text];
|
||||
}
|
||||
|
||||
$chunks = [];
|
||||
$current = '';
|
||||
|
||||
foreach ($this->semanticUnits($text) as $unit) {
|
||||
if (mb_strlen($unit) > $chunkSize) {
|
||||
if ($current !== '') {
|
||||
$chunks[] = $current;
|
||||
$current = '';
|
||||
}
|
||||
|
||||
array_push($chunks, ...$this->hardChunk($unit, $chunkSize, $chunkOverlap));
|
||||
continue;
|
||||
}
|
||||
|
||||
$candidate = $current === '' ? $unit : $current . ' ' . $unit;
|
||||
if (mb_strlen($candidate) <= $chunkSize) {
|
||||
$current = $candidate;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($current !== '') {
|
||||
$chunks[] = $current;
|
||||
}
|
||||
$current = $unit;
|
||||
}
|
||||
|
||||
if ($current !== '') {
|
||||
$chunks[] = $current;
|
||||
}
|
||||
|
||||
return $chunks === [] ? [$text] : $chunks;
|
||||
}
|
||||
|
||||
private function semanticUnits(string $text): array
|
||||
{
|
||||
$units = preg_split(self::SENTENCE_BOUNDARY_PATTERN, $text, -1, PREG_SPLIT_NO_EMPTY);
|
||||
if ($units === false || $units === []) {
|
||||
return [$text];
|
||||
}
|
||||
|
||||
return array_values(array_filter(array_map(fn (string $unit): string => $this->clean($unit), $units)));
|
||||
}
|
||||
|
||||
private function hardChunk(string $text, int $chunkSize, int $chunkOverlap): array
|
||||
{
|
||||
$length = mb_strlen($text);
|
||||
if ($length <= $chunkSize) {
|
||||
return [$text];
|
||||
}
|
||||
|
||||
$chunks = [];
|
||||
$start = 0;
|
||||
while ($start < $length) {
|
||||
$chunk = mb_substr($text, $start, $chunkSize);
|
||||
if ($chunk === '') {
|
||||
break;
|
||||
}
|
||||
|
||||
$chunks[] = $chunk;
|
||||
if ($start + $chunkSize >= $length) {
|
||||
break;
|
||||
}
|
||||
|
||||
$start += $chunkSize - $chunkOverlap;
|
||||
}
|
||||
|
||||
return $chunks;
|
||||
}
|
||||
|
||||
private function hasPageNumber(array $item): bool
|
||||
{
|
||||
return array_key_exists('page_number', $item)
|
||||
|| array_key_exists('page', $item)
|
||||
|| array_key_exists('number', $item);
|
||||
}
|
||||
|
||||
private function pageNumber(array $item): int|string
|
||||
{
|
||||
$pageNumber = $item['page_number'] ?? $item['page'] ?? $item['number'];
|
||||
return is_int($pageNumber) ? $pageNumber : $this->clean((string) $pageNumber);
|
||||
}
|
||||
|
||||
private function restorePageNumber(string $pageNumber): int|string|null
|
||||
{
|
||||
if ($pageNumber === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ctype_digit($pageNumber) ? (int) $pageNumber : $pageNumber;
|
||||
}
|
||||
|
||||
private function intOption(array $payload, string $key, int $default): int
|
||||
{
|
||||
if (!isset($payload[$key]) || $payload[$key] === '') {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return (int) $payload[$key];
|
||||
}
|
||||
|
||||
private function clean(string $value): string
|
||||
{
|
||||
return trim(preg_replace('/[ \t]+/u', ' ', $value) ?? $value);
|
||||
}
|
||||
|
||||
private function nullableClean(mixed $value): ?string
|
||||
{
|
||||
if (!is_string($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$value = $this->clean($value);
|
||||
return $value === '' ? null : $value;
|
||||
}
|
||||
|
||||
private function tagsFromString(string $value): array
|
||||
{
|
||||
$value = trim($value);
|
||||
if ($value === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$decoded = json_decode($value, true);
|
||||
if (is_array($decoded)) {
|
||||
return array_values(array_filter(array_map('strval', $decoded)));
|
||||
}
|
||||
|
||||
return array_values(array_filter(array_map('trim', preg_split('/[,,]/u', $value) ?: [])));
|
||||
}
|
||||
|
||||
private function archiveUid(array $payload): string
|
||||
{
|
||||
if (isset($payload['archive_uid']) && is_string($payload['archive_uid']) && $this->isUlid($payload['archive_uid'])) {
|
||||
return strtoupper($payload['archive_uid']);
|
||||
}
|
||||
|
||||
return (string) new Ulid();
|
||||
}
|
||||
|
||||
private function chunkUid(string $archiveUid, int $chunkIndex, string $value): string
|
||||
{
|
||||
return $archiveUid . '_' . $chunkIndex . '_' . $this->shortUid($value);
|
||||
}
|
||||
|
||||
private function shortUid(string $value): string
|
||||
{
|
||||
$number = hexdec(substr(hash('crc32b', $value), 0, 8)) % 100000;
|
||||
return str_pad((string) $number, 5, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
private function isUlid(string $value): bool
|
||||
{
|
||||
return (bool) preg_match('/^[0-9A-HJKMNP-TV-Z]{26}$/', strtoupper($value));
|
||||
}
|
||||
|
||||
private function uid(string $prefix, string $value): string
|
||||
{
|
||||
return $prefix . '_' . substr(hash('sha256', $value), 0, 24);
|
||||
}
|
||||
}
|
||||
32
app/service/LLM/LLMRequestException.php
Normal file
32
app/service/LLM/LLMRequestException.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace app\service\LLM;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class LLMRequestException extends RuntimeException
|
||||
{
|
||||
public function __construct(
|
||||
string $message,
|
||||
private readonly ?int $statusCode = null,
|
||||
private readonly ?string $providerCode = null,
|
||||
private readonly ?array $payload = null
|
||||
) {
|
||||
parent::__construct($message, $statusCode ?? 0);
|
||||
}
|
||||
|
||||
public function statusCode(): ?int
|
||||
{
|
||||
return $this->statusCode;
|
||||
}
|
||||
|
||||
public function providerCode(): ?string
|
||||
{
|
||||
return $this->providerCode;
|
||||
}
|
||||
|
||||
public function payload(): ?array
|
||||
{
|
||||
return $this->payload;
|
||||
}
|
||||
}
|
||||
54
app/service/LLM/LLMRetryQueue.php
Normal file
54
app/service/LLM/LLMRetryQueue.php
Normal file
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace app\service\LLM;
|
||||
|
||||
use Throwable;
|
||||
|
||||
class LLMRetryQueue
|
||||
{
|
||||
public function run(callable $job, array $config = []): mixed
|
||||
{
|
||||
$enabled = (bool) ($config['enabled'] ?? true);
|
||||
$maxAttempts = max(1, (int) ($config['max_attempts'] ?? 3));
|
||||
$baseDelayMs = max(0, (int) ($config['base_delay_ms'] ?? 1500));
|
||||
$maxDelayMs = max($baseDelayMs, (int) ($config['max_delay_ms'] ?? 10000));
|
||||
$retryStatuses = array_map('intval', $config['retry_statuses'] ?? [429]);
|
||||
$retryErrorCodes = array_map('strval', $config['retry_error_codes'] ?? ['1302', '1303', '1304', '1305', '1306', '1307', '1308']);
|
||||
|
||||
$attempt = 0;
|
||||
while (true) {
|
||||
$attempt++;
|
||||
|
||||
try {
|
||||
return $job($attempt);
|
||||
} catch (Throwable $exception) {
|
||||
if (!$enabled || $attempt >= $maxAttempts || !$this->shouldRetry($exception, $retryStatuses, $retryErrorCodes)) {
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
usleep($this->delayMs($attempt, $baseDelayMs, $maxDelayMs) * 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function shouldRetry(Throwable $exception, array $retryStatuses, array $retryErrorCodes): bool
|
||||
{
|
||||
if (!$exception instanceof LLMRequestException) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($exception->statusCode() !== null && in_array($exception->statusCode(), $retryStatuses, true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $exception->providerCode() !== null && in_array($exception->providerCode(), $retryErrorCodes, true);
|
||||
}
|
||||
|
||||
private function delayMs(int $attempt, int $baseDelayMs, int $maxDelayMs): int
|
||||
{
|
||||
$delay = min($maxDelayMs, $baseDelayMs * (2 ** max(0, $attempt - 1)));
|
||||
$jitter = $delay > 0 ? random_int(0, min(500, $delay)) : 0;
|
||||
|
||||
return $delay + $jitter;
|
||||
}
|
||||
}
|
||||
152
app/service/LLM/OpenAICompatibleClient.php
Normal file
152
app/service/LLM/OpenAICompatibleClient.php
Normal file
@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
namespace app\service\LLM;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
class OpenAICompatibleClient
|
||||
{
|
||||
private Client $client;
|
||||
private array $config;
|
||||
|
||||
public function __construct(?array $config = null)
|
||||
{
|
||||
$this->config = $config ?? config('LLMapi.default', []);
|
||||
$baseUrl = rtrim((string) ($this->config['base_url'] ?? ''), '/');
|
||||
|
||||
$this->client = new Client([
|
||||
'base_uri' => $baseUrl . '/',
|
||||
'timeout' => (int) ($this->config['timeout'] ?? 60),
|
||||
'connect_timeout' => (int) ($this->config['connect_timeout'] ?? 10),
|
||||
]);
|
||||
}
|
||||
|
||||
public function isConfigured(): bool
|
||||
{
|
||||
return trim((string) ($this->config['api_key'] ?? '')) !== ''
|
||||
&& trim((string) ($this->config['base_url'] ?? '')) !== '';
|
||||
}
|
||||
|
||||
public function chatJson(array $messages, array $options = []): array
|
||||
{
|
||||
$content = $this->chat($messages, $options);
|
||||
$decoded = json_decode($this->extractJson($content), true);
|
||||
|
||||
if (!is_array($decoded)) {
|
||||
throw new RuntimeException('LLM response is not valid JSON.');
|
||||
}
|
||||
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
public function chat(array $messages, array $options = []): string
|
||||
{
|
||||
if (!$this->isConfigured()) {
|
||||
throw new RuntimeException('LLM API is not configured.');
|
||||
}
|
||||
|
||||
$headers = [
|
||||
'Authorization' => 'Bearer ' . $this->config['api_key'],
|
||||
'Content-Type' => 'application/json',
|
||||
];
|
||||
|
||||
if (!empty($this->config['organization'])) {
|
||||
$headers['OpenAI-Organization'] = $this->config['organization'];
|
||||
}
|
||||
|
||||
if (!empty($this->config['project'])) {
|
||||
$headers['OpenAI-Project'] = $this->config['project'];
|
||||
}
|
||||
|
||||
$body = [
|
||||
'model' => $options['model'] ?? config('LLMapi.chat.model'),
|
||||
'messages' => $messages,
|
||||
'temperature' => $options['temperature'] ?? config('LLMapi.chat.temperature', 0.2),
|
||||
'max_tokens' => $options['max_tokens'] ?? config('LLMapi.chat.max_tokens', 1200),
|
||||
'stream' => (bool) ($options['stream'] ?? config('LLMapi.chat.stream', false)),
|
||||
];
|
||||
|
||||
if (array_key_exists('response_format', $options) && is_array($options['response_format'])) {
|
||||
$body['response_format'] = $options['response_format'];
|
||||
}
|
||||
|
||||
if (array_key_exists('thinking', $options) && is_array($options['thinking'])) {
|
||||
$body['thinking'] = $options['thinking'];
|
||||
}
|
||||
|
||||
if (array_key_exists('request_id', $options) && is_string($options['request_id'])) {
|
||||
$body['request_id'] = $options['request_id'];
|
||||
}
|
||||
|
||||
if (array_key_exists('user_id', $options) && is_string($options['user_id'])) {
|
||||
$body['user_id'] = $options['user_id'];
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $this->client->post('chat/completions', [
|
||||
'headers' => $headers,
|
||||
'json' => $body,
|
||||
]);
|
||||
} catch (RequestException $exception) {
|
||||
throw $this->requestException($exception);
|
||||
} catch (Throwable $exception) {
|
||||
throw new RuntimeException('LLM chat request failed: ' . $exception->getMessage(), 0, $exception);
|
||||
}
|
||||
|
||||
$payload = json_decode((string) $response->getBody(), true);
|
||||
$content = $payload['choices'][0]['message']['content'] ?? null;
|
||||
|
||||
if (!is_string($content) || trim($content) === '') {
|
||||
throw new RuntimeException('LLM chat response is empty.');
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
private function extractJson(string $content): string
|
||||
{
|
||||
$content = trim($content);
|
||||
$content = preg_replace('/^```(?:json)?\s*/i', '', $content) ?? $content;
|
||||
$content = preg_replace('/\s*```$/', '', $content) ?? $content;
|
||||
|
||||
$start = strpos($content, '{');
|
||||
$end = strrpos($content, '}');
|
||||
if ($start !== false && $end !== false && $end > $start) {
|
||||
return substr($content, $start, $end - $start + 1);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
private function requestException(RequestException $exception): LLMRequestException
|
||||
{
|
||||
$statusCode = $exception->getResponse()?->getStatusCode();
|
||||
$body = $exception->getResponse() ? (string) $exception->getResponse()->getBody() : '';
|
||||
$payload = json_decode($body, true);
|
||||
$providerCode = null;
|
||||
$providerMessage = null;
|
||||
|
||||
if (is_array($payload)) {
|
||||
$providerCode = isset($payload['error']['code']) ? (string) $payload['error']['code'] : null;
|
||||
$providerMessage = isset($payload['error']['message']) ? (string) $payload['error']['message'] : null;
|
||||
}
|
||||
|
||||
$message = 'LLM chat request failed';
|
||||
if ($statusCode !== null) {
|
||||
$message .= " with HTTP {$statusCode}";
|
||||
}
|
||||
if ($providerCode !== null) {
|
||||
$message .= " and provider code {$providerCode}";
|
||||
}
|
||||
if ($providerMessage !== null) {
|
||||
$message .= ": {$providerMessage}";
|
||||
} else {
|
||||
$message .= ': ' . $exception->getMessage();
|
||||
}
|
||||
|
||||
return new LLMRequestException($message, $statusCode, $providerCode, is_array($payload) ? $payload : null);
|
||||
}
|
||||
}
|
||||
14
app/view/index/view.html
Normal file
14
app/view/index/view.html
Normal file
@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="shortcut icon" href="/favicon.ico"/>
|
||||
<title>webman</title>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
hello <?=htmlspecialchars($name)?>
|
||||
</body>
|
||||
</html>
|
||||
29
ark.txt
Normal file
29
ark.txt
Normal file
@ -0,0 +1,29 @@
|
||||
archives
|
||||
- id 主键、自增
|
||||
- archive_uid 非空
|
||||
- title 主题
|
||||
- summary 档案摘要
|
||||
- year 年份
|
||||
- author 作者
|
||||
- source 来源
|
||||
- series 所属系列集
|
||||
- tags JSON
|
||||
- metadata JSON
|
||||
- content OCR识别全文存放地址
|
||||
- raw 原始pdf/图片存放地址
|
||||
- chunks JSON
|
||||
- created_time 入库时间
|
||||
- updated_time 修改时间
|
||||
|
||||
chunks
|
||||
- id 主键、自增
|
||||
- chunk_uid 非空
|
||||
- archive_uid 所属档案ID
|
||||
- chunk_index chunk序号
|
||||
- page_start
|
||||
- page_end
|
||||
- text chunk文本
|
||||
- length
|
||||
- embedding_status INT 0=none, 1=OpenSearch
|
||||
- embedding_ref JSON
|
||||
- embedding_model TEXT 格式例如 fastembedding/1.0
|
||||
71
composer.json
Normal file
71
composer.json
Normal file
@ -0,0 +1,71 @@
|
||||
{
|
||||
"name": "workerman/webman",
|
||||
"type": "project",
|
||||
"keywords": [
|
||||
"high performance",
|
||||
"http service"
|
||||
],
|
||||
"homepage": "https://www.workerman.net",
|
||||
"license": "MIT",
|
||||
"description": "High performance HTTP Service Framework.",
|
||||
"authors": [
|
||||
{
|
||||
"name": "walkor",
|
||||
"email": "walkor@workerman.net",
|
||||
"homepage": "https://www.workerman.net",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"support": {
|
||||
"email": "walkor@workerman.net",
|
||||
"issues": "https://github.com/walkor/webman/issues",
|
||||
"forum": "https://wenda.workerman.net/",
|
||||
"wiki": "https://workerman.net/doc/webman",
|
||||
"source": "https://github.com/walkor/webman"
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"workerman/webman-framework": "^2.1",
|
||||
"monolog/monolog": "^2.0",
|
||||
"webman/console": "^2.2",
|
||||
"webman/database": "^2.1",
|
||||
"illuminate/pagination": "^13.7",
|
||||
"illuminate/events": "^13.7",
|
||||
"symfony/var-dumper": "^8.0",
|
||||
"webman/redis": "^2.1",
|
||||
"webman/validation": "^2.2",
|
||||
"symfony/uid": "^8.0",
|
||||
"vlucas/phpdotenv": "^5.6",
|
||||
"guzzlehttp/guzzle": "^7.10"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-event": "For better performance. "
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"": "./",
|
||||
"app\\": "./app",
|
||||
"App\\": "./app",
|
||||
"app\\View\\Components\\": "./app/view/components"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"post-package-install": [
|
||||
"support\\Plugin::install"
|
||||
],
|
||||
"post-package-update": [
|
||||
"support\\Plugin::install"
|
||||
],
|
||||
"pre-package-uninstall": [
|
||||
"support\\Plugin::uninstall"
|
||||
],
|
||||
"post-create-project-cmd": [
|
||||
"support\\Setup::run"
|
||||
],
|
||||
"setup-webman": [
|
||||
"support\\Setup::run"
|
||||
]
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true
|
||||
}
|
||||
5398
composer.lock
generated
Normal file
5398
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
57
config/LLMapi.php
Normal file
57
config/LLMapi.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
/*
|
||||
* OpenAI-compatible API configuration.
|
||||
*
|
||||
* Environment variables are optional, but recommended in deployment:
|
||||
* - LLM_API_BASE_URL
|
||||
* - LLM_API_KEY
|
||||
* - LLM_CHAT_MODEL
|
||||
* - LLM_EMBEDDING_MODEL
|
||||
*/
|
||||
'default' => [
|
||||
'base_url' => getenv('LLM_API_BASE_URL') ?: 'https://api.openai.com/v1',
|
||||
'api_key' => getenv('LLM_API_KEY') ?: '',
|
||||
'organization' => getenv('LLM_ORGANIZATION') ?: null,
|
||||
'project' => getenv('LLM_PROJECT') ?: null,
|
||||
'timeout' => (int) (getenv('LLM_API_TIMEOUT') ?: 60),
|
||||
'connect_timeout' => (int) (getenv('LLM_API_CONNECT_TIMEOUT') ?: 10),
|
||||
],
|
||||
|
||||
'chat' => [
|
||||
'model' => getenv('LLM_CHAT_MODEL') ?: 'gpt-4.1-mini',
|
||||
'temperature' => (float) (getenv('LLM_CHAT_TEMPERATURE') ?: 0.2),
|
||||
'max_tokens' => (int) (getenv('LLM_CHAT_MAX_TOKENS') ?: 1200),
|
||||
'stream' => false,
|
||||
],
|
||||
|
||||
'metadata' => [
|
||||
'enabled' => (getenv('LLM_METADATA_ENABLED') ?: 'true') !== 'false',
|
||||
'model' => getenv('LLM_METADATA_MODEL') ?: (getenv('LLM_CHAT_MODEL') ?: 'gpt-4.1-mini'),
|
||||
'max_input_chars' => (int) (getenv('LLM_METADATA_MAX_INPUT_CHARS') ?: 12000),
|
||||
'max_tokens' => (int) (getenv('LLM_METADATA_MAX_TOKENS') ?: 1200),
|
||||
'temperature' => (float) (getenv('LLM_METADATA_TEMPERATURE') ?: 0.1),
|
||||
'stream' => false,
|
||||
'response_format' => ['type' => 'json_object'],
|
||||
'thinking' => [
|
||||
'type' => getenv('LLM_METADATA_THINKING') ?: 'disabled',
|
||||
],
|
||||
'retry' => [
|
||||
'enabled' => (getenv('LLM_METADATA_RETRY_ENABLED') ?: 'true') !== 'false',
|
||||
'max_attempts' => (int) (getenv('LLM_METADATA_RETRY_MAX_ATTEMPTS') ?: 3),
|
||||
'base_delay_ms' => (int) (getenv('LLM_METADATA_RETRY_BASE_DELAY_MS') ?: 1500),
|
||||
'max_delay_ms' => (int) (getenv('LLM_METADATA_RETRY_MAX_DELAY_MS') ?: 10000),
|
||||
'retry_statuses' => [429],
|
||||
'retry_error_codes' => ['1302', '1303', '1304', '1305', '1306', '1307', '1308'],
|
||||
],
|
||||
],
|
||||
|
||||
'embedding' => [
|
||||
'model' => getenv('LLM_EMBEDDING_MODEL') ?: 'text-embedding-3-small',
|
||||
'batch_size' => (int) (getenv('LLM_EMBEDDING_BATCH_SIZE') ?: 64),
|
||||
'dimensions' => getenv('LLM_EMBEDDING_DIMENSIONS') !== false
|
||||
? (int) getenv('LLM_EMBEDDING_DIMENSIONS')
|
||||
: null,
|
||||
],
|
||||
];
|
||||
26
config/app.php
Normal file
26
config/app.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
use support\Request;
|
||||
|
||||
return [
|
||||
'debug' => true,
|
||||
'error_reporting' => E_ALL,
|
||||
'default_timezone' => 'Asia/Shanghai',
|
||||
'request_class' => Request::class,
|
||||
'public_path' => base_path() . DIRECTORY_SEPARATOR . 'public',
|
||||
'runtime_path' => base_path(false) . DIRECTORY_SEPARATOR . 'runtime',
|
||||
'controller_suffix' => 'Controller',
|
||||
'controller_reuse' => false,
|
||||
];
|
||||
21
config/autoload.php
Normal file
21
config/autoload.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
return [
|
||||
'files' => [
|
||||
base_path() . '/app/functions.php',
|
||||
base_path() . '/support/Request.php',
|
||||
base_path() . '/support/Response.php',
|
||||
]
|
||||
];
|
||||
17
config/bootstrap.php
Normal file
17
config/bootstrap.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
return [
|
||||
support\bootstrap\Session::class,
|
||||
];
|
||||
15
config/container.php
Normal file
15
config/container.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
return new Webman\Container;
|
||||
25
config/database.php
Normal file
25
config/database.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
return [
|
||||
'default' => 'postgre',
|
||||
'connections' => [
|
||||
'postgre' => [
|
||||
'driver' => 'pgsql',
|
||||
'host' => 'layfi.postgres.database.azure.com',
|
||||
'port' => 5432,
|
||||
'database' => 'proofdb',
|
||||
'username' => 'proofdb',
|
||||
'password' => 'proofdb',
|
||||
'charset' => 'utf8',
|
||||
'prefix' => '',
|
||||
'schema' => 'public',
|
||||
'sslmode' => 'prefer',
|
||||
'pool' => [ // 连接池配置,仅支持swoole/swow驱动
|
||||
'max_connections' => 5, // 最大连接数
|
||||
'min_connections' => 1, // 最小连接数
|
||||
'wait_timeout' => 3, // 从连接池获取连接等待的最大时间,超时后会抛出异常
|
||||
'idle_timeout' => 60, // 连接池中连接最大空闲时间,超时后会关闭回收,直到连接数为min_connections
|
||||
'heartbeat_interval' => 50, // 连接池心跳检测时间,单位秒,建议小于60秒
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
15
config/dependence.php
Normal file
15
config/dependence.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
return [];
|
||||
17
config/exception.php
Normal file
17
config/exception.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
return [
|
||||
'' => support\exception\Handler::class,
|
||||
];
|
||||
32
config/log.php
Normal file
32
config/log.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
return [
|
||||
'default' => [
|
||||
'handlers' => [
|
||||
[
|
||||
'class' => Monolog\Handler\RotatingFileHandler::class,
|
||||
'constructor' => [
|
||||
runtime_path() . '/logs/webman.log',
|
||||
7, //$maxFiles
|
||||
Monolog\Logger::DEBUG,
|
||||
],
|
||||
'formatter' => [
|
||||
'class' => Monolog\Formatter\LineFormatter::class,
|
||||
'constructor' => [null, 'Y-m-d H:i:s', true],
|
||||
],
|
||||
]
|
||||
],
|
||||
],
|
||||
];
|
||||
15
config/middleware.php
Normal file
15
config/middleware.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
return [];
|
||||
28
config/plugin/webman/console/app.php
Normal file
28
config/plugin/webman/console/app.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
return [
|
||||
'enable' => true,
|
||||
|
||||
'build_dir' => BASE_PATH . DIRECTORY_SEPARATOR . 'build',
|
||||
|
||||
'phar_filename' => 'webman.phar',
|
||||
|
||||
'phar_format' => Phar::PHAR, // Phar archive format: Phar::PHAR, Phar::TAR, Phar::ZIP
|
||||
|
||||
'phar_compression' => Phar::NONE, // Compression method for Phar archive: Phar::NONE, Phar::GZ, Phar::BZ2
|
||||
|
||||
'bin_filename' => 'webman.bin',
|
||||
|
||||
'signature_algorithm'=> Phar::SHA256, //set the signature algorithm for a phar and apply it. The signature algorithm must be one of Phar::MD5, Phar::SHA1, Phar::SHA256, Phar::SHA512, or Phar::OPENSSL.
|
||||
|
||||
'private_key_file' => '', // The file path for certificate or OpenSSL private key file.
|
||||
|
||||
'exclude_pattern' => '#^(?!.*(composer.json|/.github/|/.idea/|/.git/|/.setting/|/runtime/|/vendor-bin/|/build/|/vendor/webman/admin/))(.*)$#',
|
||||
|
||||
'exclude_files' => [
|
||||
'.env', 'LICENSE', 'composer.json', 'composer.lock', 'start.php', 'webman.phar', 'webman.bin'
|
||||
],
|
||||
|
||||
'custom_ini' => '
|
||||
memory_limit = 256M
|
||||
',
|
||||
];
|
||||
8
config/plugin/webman/validation/app.php
Normal file
8
config/plugin/webman/validation/app.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
use support\validation\ValidationException;
|
||||
|
||||
return [
|
||||
'enable' => true,
|
||||
'exception' => ValidationException::class,
|
||||
];
|
||||
7
config/plugin/webman/validation/command.php
Normal file
7
config/plugin/webman/validation/command.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Webman\Validation\Command\MakeValidatorCommand;
|
||||
|
||||
return [
|
||||
MakeValidatorCommand::class
|
||||
];
|
||||
9
config/plugin/webman/validation/middleware.php
Normal file
9
config/plugin/webman/validation/middleware.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
use Webman\Validation\Middleware;
|
||||
|
||||
return [
|
||||
'@' => [
|
||||
Middleware::class,
|
||||
],
|
||||
];
|
||||
68
config/process.php
Normal file
68
config/process.php
Normal file
@ -0,0 +1,68 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
use support\Log;
|
||||
use support\Request;
|
||||
use app\process\Http;
|
||||
|
||||
global $argv;
|
||||
|
||||
return [
|
||||
'webman' => [
|
||||
'handler' => Http::class,
|
||||
'listen' => 'http://0.0.0.0:8787',
|
||||
'count' => cpu_count() * 4,
|
||||
'user' => '',
|
||||
'group' => '',
|
||||
'reusePort' => false,
|
||||
'eventLoop' => '',
|
||||
'context' => [],
|
||||
'constructor' => [
|
||||
'requestClass' => Request::class,
|
||||
'logger' => Log::channel('default'),
|
||||
'appPath' => app_path(),
|
||||
'publicPath' => public_path()
|
||||
]
|
||||
],
|
||||
// File update detection and automatic reload
|
||||
'monitor' => [
|
||||
'handler' => app\process\Monitor::class,
|
||||
'reloadable' => false,
|
||||
'constructor' => [
|
||||
// Monitor these directories
|
||||
'monitorDir' => array_merge([
|
||||
app_path(),
|
||||
config_path(),
|
||||
base_path() . '/process',
|
||||
base_path() . '/support',
|
||||
base_path() . '/resource',
|
||||
base_path() . '/.env',
|
||||
], glob(base_path() . '/plugin/*/app'), glob(base_path() . '/plugin/*/config'), glob(base_path() . '/plugin/*/api')),
|
||||
// Files with these suffixes will be monitored
|
||||
'monitorExtensions' => [
|
||||
'php', 'html', 'htm', 'env'
|
||||
],
|
||||
'options' => [
|
||||
'enable_file_monitor' => !in_array('-d', $argv) && DIRECTORY_SEPARATOR === '/',
|
||||
'enable_memory_monitor' => DIRECTORY_SEPARATOR === '/',
|
||||
]
|
||||
]
|
||||
],
|
||||
'ai_metadata' => [
|
||||
'handler' => app\process\AiMetadata::class,
|
||||
'count' => 1,
|
||||
'reloadable' => true,
|
||||
'constructor' => []
|
||||
]
|
||||
];
|
||||
14
config/queue.php
Normal file
14
config/queue.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'ai_metadata' => [
|
||||
'pending' => 'proofdb:ai:metadata:pending',
|
||||
'delayed' => 'proofdb:ai:metadata:delayed',
|
||||
'failed' => 'proofdb:ai:metadata:failed',
|
||||
'retry_prefix' => 'proofdb:ai:metadata:retry:',
|
||||
'error_prefix' => 'proofdb:ai:metadata:error:',
|
||||
'max_retries' => (int) (getenv('AI_METADATA_QUEUE_MAX_RETRIES') ?: 5),
|
||||
'base_delay_seconds' => (int) (getenv('AI_METADATA_QUEUE_BASE_DELAY_SECONDS') ?: 60),
|
||||
'block_timeout' => (int) (getenv('AI_METADATA_QUEUE_BLOCK_TIMEOUT') ?: 5),
|
||||
],
|
||||
];
|
||||
29
config/redis.php
Normal file
29
config/redis.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
return [
|
||||
'default' => [
|
||||
'password' => 'qi2005112',
|
||||
'host' => '127.0.0.1',
|
||||
'port' => 6379,
|
||||
'database' => 0,
|
||||
'pool' => [
|
||||
'max_connections' => 5,
|
||||
'min_connections' => 1,
|
||||
'wait_timeout' => 3,
|
||||
'idle_timeout' => 60,
|
||||
'heartbeat_interval' => 50,
|
||||
],
|
||||
]
|
||||
];
|
||||
21
config/route.php
Normal file
21
config/route.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
use Webman\Route;
|
||||
|
||||
Route::post('/api/articles/import', [app\controller\Api\ArticleImportController::class, 'import']);
|
||||
|
||||
|
||||
|
||||
|
||||
23
config/server.php
Normal file
23
config/server.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
return [
|
||||
'event_loop' => '',
|
||||
'stop_timeout' => 2,
|
||||
'pid_file' => runtime_path() . '/webman.pid',
|
||||
'status_file' => runtime_path() . '/webman.status',
|
||||
'stdout_file' => runtime_path() . '/logs/stdout.log',
|
||||
'log_file' => runtime_path() . '/logs/workerman.log',
|
||||
'max_package_size' => 10 * 1024 * 1024
|
||||
];
|
||||
65
config/session.php
Normal file
65
config/session.php
Normal file
@ -0,0 +1,65 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
use Webman\Session\FileSessionHandler;
|
||||
use Webman\Session\RedisSessionHandler;
|
||||
use Webman\Session\RedisClusterSessionHandler;
|
||||
|
||||
return [
|
||||
|
||||
'type' => 'file', // or redis or redis_cluster
|
||||
|
||||
'handler' => FileSessionHandler::class,
|
||||
|
||||
'config' => [
|
||||
'file' => [
|
||||
'save_path' => runtime_path() . '/sessions',
|
||||
],
|
||||
'redis' => [
|
||||
'host' => '127.0.0.1',
|
||||
'port' => 6379,
|
||||
'auth' => '',
|
||||
'timeout' => 2,
|
||||
'database' => '',
|
||||
'prefix' => 'redis_session_',
|
||||
],
|
||||
'redis_cluster' => [
|
||||
'host' => ['127.0.0.1:7000', '127.0.0.1:7001', '127.0.0.1:7001'],
|
||||
'timeout' => 2,
|
||||
'auth' => '',
|
||||
'prefix' => 'redis_session_',
|
||||
]
|
||||
],
|
||||
|
||||
'session_name' => 'PHPSID',
|
||||
|
||||
'auto_update_timestamp' => false,
|
||||
|
||||
'lifetime' => 7*24*60*60,
|
||||
|
||||
'cookie_lifetime' => 365*24*60*60,
|
||||
|
||||
'cookie_path' => '/',
|
||||
|
||||
'domain' => '',
|
||||
|
||||
'http_only' => true,
|
||||
|
||||
'secure' => false,
|
||||
|
||||
'same_site' => '',
|
||||
|
||||
'gc_probability' => [1, 1000],
|
||||
|
||||
];
|
||||
23
config/static.php
Normal file
23
config/static.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
/**
|
||||
* Static file settings
|
||||
*/
|
||||
return [
|
||||
'enable' => true,
|
||||
'middleware' => [ // Static file Middleware
|
||||
//app\middleware\StaticFile::class,
|
||||
],
|
||||
];
|
||||
25
config/translation.php
Normal file
25
config/translation.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
/**
|
||||
* Multilingual configuration
|
||||
*/
|
||||
return [
|
||||
// Default language
|
||||
'locale' => 'zh_CN',
|
||||
// Fallback language
|
||||
'fallback_locale' => ['zh_CN', 'en'],
|
||||
// Folder where language files are stored
|
||||
'path' => base_path() . '/resource/translations',
|
||||
];
|
||||
22
config/view.php
Normal file
22
config/view.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
use support\view\Raw;
|
||||
use support\view\Twig;
|
||||
use support\view\Blade;
|
||||
use support\view\ThinkPHP;
|
||||
|
||||
return [
|
||||
'handler' => Raw::class
|
||||
];
|
||||
11
docker-compose.yml
Normal file
11
docker-compose.yml
Normal file
@ -0,0 +1,11 @@
|
||||
version: "3"
|
||||
services:
|
||||
webman:
|
||||
build: .
|
||||
container_name: docker-webman
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- "./:/app"
|
||||
ports:
|
||||
- "8787:8787"
|
||||
command: ["php", "start.php", "start" ]
|
||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
214
readme.md
Normal file
214
readme.md
Normal file
@ -0,0 +1,214 @@
|
||||
## 1. Project Overview
|
||||
|
||||
**Project Name:** Proof DB
|
||||
**Type:** Historical Evidence Retrieval System (RAG-oriented backend)
|
||||
|
||||
This project is a **backend-centric system** designed to manage, index, and retrieve historical evidence (documents, archives, OCR text) with strong emphasis on:
|
||||
|
||||
* Evidence traceability
|
||||
* Chunk-level retrieval
|
||||
* Hybrid search (full-text + vector)
|
||||
* Citation reconstruction
|
||||
|
||||
Unlike generic RAG systems, this project treats **"evidence" as first-class structured objects**, not just text.
|
||||
|
||||
---
|
||||
|
||||
## 2. Core Concept
|
||||
|
||||
The system is divided into three conceptual layers:
|
||||
|
||||
* **Proof DB** → Data layer (MySQL + OpenSearch + Vector)
|
||||
* **Archive Cask** → Frontend interface (not part of this task)
|
||||
* **Few-shot Engine** → OCR (external, not part of this task)
|
||||
|
||||
Current scope: **Proof DB only**
|
||||
|
||||
---
|
||||
|
||||
## 3. System Architecture (Backend Focus)
|
||||
|
||||
The backend follows a **modular service architecture** (not microservices yet, but clearly separated layers):
|
||||
|
||||
### Components:
|
||||
|
||||
1. **Ingestion Layer**
|
||||
|
||||
* Accepts raw Markdown archive documents
|
||||
* Pre-processes Markdown page markers such as `<!-- DOCMASTER:PAGE 0001 -->`
|
||||
* Splits documents into page-bounded vector chunks
|
||||
* Keeps list-style archive records and their `COMMENT` blocks together where possible
|
||||
* Extracts metadata, including page numbers
|
||||
* Enqueues missing archive metadata such as title, year, author, tags, and summary for async LLM enrichment
|
||||
|
||||
2. **Storage Layer**
|
||||
|
||||
* MySQL → metadata, relations
|
||||
* OpenSearch → full-text index
|
||||
* Vector DB → embeddings (can be OpenSearch kNN or Qdrant)
|
||||
|
||||
3. **Retrieval Layer**
|
||||
|
||||
* Full-text search (BM25)
|
||||
* Vector search (embedding similarity)
|
||||
* Hybrid search (fusion)
|
||||
|
||||
4. **Evidence Layer**
|
||||
|
||||
* Maps chunk → page → article
|
||||
* Provides page-level citation traceability
|
||||
|
||||
👉 这是典型 backend architecture 分层设计(server + database + API协同) ([DEV Community][1])
|
||||
|
||||
---
|
||||
|
||||
## 4. Tech Stack
|
||||
|
||||
### Backend Framework
|
||||
|
||||
* PHP 8+
|
||||
* Webman (HTTP API)
|
||||
* Workerman (async workers / background jobs)
|
||||
|
||||
### Database
|
||||
|
||||
* MySQL (relational metadata)
|
||||
|
||||
### Search Engine
|
||||
|
||||
* OpenSearch
|
||||
|
||||
* Full-text search (BM25)
|
||||
* Optional vector search (kNN)
|
||||
|
||||
### Vector Layer
|
||||
|
||||
* Option A: OpenSearch kNN
|
||||
* Option B: Qdrant (preferred if scaling)
|
||||
|
||||
### Data Flow Tools
|
||||
|
||||
* Custom chunking logic (PHP)
|
||||
* Embedding via external API / local model
|
||||
* Metadata enrichment via Redis queue + OpenAI-compatible chat completion API
|
||||
|
||||
---
|
||||
|
||||
## 5. Data Model (CRITICAL)
|
||||
|
||||
### Core Entities
|
||||
|
||||
```text
|
||||
Archive
|
||||
├── archive_uid (ULID)
|
||||
├── title
|
||||
├── summary
|
||||
├── source
|
||||
└── metadata
|
||||
|
||||
Page
|
||||
├── page_number
|
||||
├── block_count
|
||||
├── chunk_count
|
||||
└── content_length
|
||||
|
||||
PageBlock (internal import structure)
|
||||
├── block_uid
|
||||
├── archive_uid
|
||||
├── page_number
|
||||
└── content
|
||||
|
||||
Chunk
|
||||
├── chunk_uid (archive_uid + sequence + short uid)
|
||||
├── page_start
|
||||
├── page_end
|
||||
├── text
|
||||
├── embedding_ref
|
||||
```
|
||||
|
||||
### Key Principle
|
||||
|
||||
* **archive_uid 是档案级核心 ID,使用 ULID**
|
||||
* **chunk_uid 是 chunk 级核心 ID,格式为 `{archive_uid}_{chunk_index}_{short_uid}`**
|
||||
* MySQL / OpenSearch / Vector DB 全部围绕 `archive_uid` 和 `chunk_uid`
|
||||
* **page_number 是证据定位的关键字段**
|
||||
* Chunk 是向量化和检索召回单位,不是精确 citation 单位
|
||||
* 证据定位只需要定位到页码,因此 chunk 可以跨段落合并,但不能跨页
|
||||
|
||||
---
|
||||
|
||||
## 6. Search Design
|
||||
|
||||
### Full-text (OpenSearch)
|
||||
|
||||
* Indexed at chunk level
|
||||
* Supports:
|
||||
|
||||
* keyword match
|
||||
* phrase match
|
||||
|
||||
### Vector Search
|
||||
|
||||
* embedding similarity
|
||||
|
||||
### Hybrid Search
|
||||
|
||||
* BM25 + vector fusion
|
||||
* rerank stage
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 7. API Design (First Phase)
|
||||
|
||||
### Ingestion
|
||||
|
||||
```http
|
||||
POST /api/articles/import
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Retrieval
|
||||
|
||||
```http
|
||||
POST /api/search/fulltext
|
||||
POST /api/search/vector
|
||||
POST /api/search/hybrid
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Evidence
|
||||
|
||||
```http
|
||||
GET /api/chunks/{chunk_uid}
|
||||
GET /api/evidence/{chunk_uid}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Design Philosophy (IMPORTANT)
|
||||
|
||||
* Evidence > Text
|
||||
* Chunk > Document
|
||||
* Traceability > Raw Retrieval
|
||||
* Hybrid Search by default
|
||||
|
||||
---
|
||||
|
||||
## 9. Non-goals (IMPORTANT)
|
||||
|
||||
* No frontend (Archive Cask handled later)
|
||||
* No OCR (Few-shot Engine external)
|
||||
* No heavy microservices (keep simple modular architecture first)
|
||||
|
||||
|
||||
|
||||
* Proof DB ≠ storage
|
||||
* Proof DB = retrieval + meaning + traceability
|
||||
|
||||
|
||||
[1]: https://dev.to/tomjohnson3/understanding-backend-architecture-ljb?utm_source=chatgpt.com "Understanding Backend Architecture"
|
||||
[2]: https://exodata.io/what-is-a-tech-stack-how-to-architect-a-modern-scalable-technology-stack/?utm_source=chatgpt.com "How to Build a Tech Stack That Scales [2026] | Exodata"
|
||||
[3]: https://medium.com/%40hanxuyang0826/roadmap-to-backend-programming-master-architectural-patterns-c763c9194414?utm_source=chatgpt.com "Roadmap to Backend Programming Master: Architectural ..."
|
||||
4
runtime/.gitignore
vendored
Normal file
4
runtime/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
*
|
||||
!logs
|
||||
!views
|
||||
!.gitignore
|
||||
2
runtime/logs/.gitignore
vendored
Normal file
2
runtime/logs/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
2
runtime/views/.gitignore
vendored
Normal file
2
runtime/views/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
89
scripts/setup_database.php
Normal file
89
scripts/setup_database.php
Normal file
@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use support\Db;
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
require __DIR__ . '/../support/bootstrap.php';
|
||||
require __DIR__ . '/../vendor/webman/database/src/support/Db.php';
|
||||
|
||||
$statements = [
|
||||
<<<SQL
|
||||
CREATE TABLE IF NOT EXISTS archives (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
archive_uid VARCHAR(26) NOT NULL UNIQUE,
|
||||
title TEXT,
|
||||
summary TEXT,
|
||||
year INTEGER,
|
||||
author TEXT,
|
||||
source TEXT,
|
||||
series TEXT,
|
||||
tags JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
content TEXT,
|
||||
raw TEXT,
|
||||
chunks JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
created_time TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_time TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
SQL,
|
||||
'ALTER TABLE archives ADD COLUMN IF NOT EXISTS summary TEXT',
|
||||
<<<SQL
|
||||
CREATE TABLE IF NOT EXISTS chunks (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
chunk_uid VARCHAR(64) NOT NULL UNIQUE,
|
||||
archive_uid VARCHAR(26) NOT NULL,
|
||||
chunk_index INTEGER NOT NULL,
|
||||
page_start INTEGER,
|
||||
page_end INTEGER,
|
||||
text TEXT,
|
||||
length INTEGER,
|
||||
embedding_status INTEGER NOT NULL DEFAULT 0,
|
||||
embedding_ref JSONB,
|
||||
embedding_model TEXT,
|
||||
CONSTRAINT chunks_archive_uid_foreign
|
||||
FOREIGN KEY (archive_uid)
|
||||
REFERENCES archives (archive_uid)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT chunks_archive_index_unique
|
||||
UNIQUE (archive_uid, chunk_index)
|
||||
)
|
||||
SQL,
|
||||
'CREATE INDEX IF NOT EXISTS archives_year_index ON archives (year)',
|
||||
'CREATE INDEX IF NOT EXISTS archives_series_index ON archives (series)',
|
||||
'CREATE INDEX IF NOT EXISTS archives_tags_gin_index ON archives USING GIN (tags)',
|
||||
'CREATE INDEX IF NOT EXISTS archives_metadata_gin_index ON archives USING GIN (metadata)',
|
||||
'CREATE INDEX IF NOT EXISTS chunks_archive_uid_index ON chunks (archive_uid)',
|
||||
'CREATE INDEX IF NOT EXISTS chunks_page_range_index ON chunks (archive_uid, page_start, page_end)',
|
||||
'CREATE INDEX IF NOT EXISTS chunks_embedding_status_index ON chunks (embedding_status)',
|
||||
<<<SQL
|
||||
CREATE OR REPLACE FUNCTION set_updated_time()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_time = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql
|
||||
SQL,
|
||||
'DROP TRIGGER IF EXISTS archives_set_updated_time ON archives',
|
||||
<<<SQL
|
||||
CREATE TRIGGER archives_set_updated_time
|
||||
BEFORE UPDATE ON archives
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_updated_time()
|
||||
SQL,
|
||||
];
|
||||
|
||||
try {
|
||||
Db::connection()->getPdo();
|
||||
echo 'Database connection ok: ' . config('database.default') . PHP_EOL;
|
||||
|
||||
foreach ($statements as $statement) {
|
||||
Db::statement($statement);
|
||||
}
|
||||
|
||||
echo 'Tables initialized: archives, chunks' . PHP_EOL;
|
||||
} catch (Throwable $exception) {
|
||||
fwrite(STDERR, $exception::class . ': ' . $exception->getMessage() . PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
5
start.php
Executable file
5
start.php
Executable file
@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
chdir(__DIR__);
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
support\App::run();
|
||||
24
support/Request.php
Normal file
24
support/Request.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
namespace support;
|
||||
|
||||
/**
|
||||
* Class Request
|
||||
* @package support
|
||||
*/
|
||||
class Request extends \Webman\Http\Request
|
||||
{
|
||||
|
||||
}
|
||||
24
support/Response.php
Normal file
24
support/Response.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
namespace support;
|
||||
|
||||
/**
|
||||
* Class Response
|
||||
* @package support
|
||||
*/
|
||||
class Response extends \Webman\Http\Response
|
||||
{
|
||||
|
||||
}
|
||||
1558
support/Setup.php
Normal file
1558
support/Setup.php
Normal file
File diff suppressed because it is too large
Load Diff
3
support/bootstrap.php
Normal file
3
support/bootstrap.php
Normal file
@ -0,0 +1,3 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../vendor/workerman/webman-framework/src/support/bootstrap.php';
|
||||
329
test/1.test.md
Normal file
329
test/1.test.md
Normal file
@ -0,0 +1,329 @@
|
||||
<!-- DOCMASTER:PAGE 0001 -->
|
||||
|
||||
## Page 1
|
||||
|
||||
UNCLASSIFIED with ~~TOP SECRET~~ Attachments
|
||||
* *UNCLASSIFIED**
|
||||
21220
|
||||
|
||||
THE WHITE HOUSE
|
||||
WASHINGTON
|
||||
|
||||
January 3, 1992
|
||||
|
||||
NATIONAL SECURITY DIRECTIVE 76
|
||||
|
||||
MEMORANDUM FOR THE VICE PRESIDENT
|
||||
THE SECRETARY OF STATE
|
||||
THE SECRETARY OF THE TREASURY
|
||||
THE SECRETARY OF DEFENSE
|
||||
THE ATTORNEY GENERAL
|
||||
THE DIRECTOR, OFFICE OF MANAGEMENT & BUDGET
|
||||
THE DIRECTOR OF CENTRAL INTELLIGENCE
|
||||
|
||||
SUBJECT: Disposition of NSC Policy Documents
|
||||
|
||||
After an interagency review of active NSC Policy Documents, I direct that the following disposition be made:
|
||||
|
||||
1. NSC Policy Documents listed at Tab A are no longer in force and have been superseded by more recent policy directives.
|
||||
2. NSC Policy Documents listed at Tab B have been completed and are no longer in force.
|
||||
|
||||
This directive supplements NSD 59.
|
||||
|
||||
Brent Scowcroft
|
||||
|
||||
Attachments
|
||||
Tab A Listing of Superseded Policy Documents
|
||||
Tab B Listing of Completed NSC Policy Documents
|
||||
|
||||
UNCLASSIFIED with ~~TOP SECRET~~ Attachments
|
||||
* *UNCLASSIFIED**
|
||||
|
||||
Partially Declassified/Released on 10-17-96
|
||||
under provisions of E.O. 12958
|
||||
by O. Van Tassel, National Security Council
|
||||
F 89-191
|
||||
|
||||
---
|
||||
|
||||
<!-- DOCMASTER:PAGE 0002 -->
|
||||
|
||||
## Page 2
|
||||
|
||||
~~TOP SECRET~~
|
||||
# UNCLASSIFIED
|
||||
21220
|
||||
|
||||
NO LONGER IN FORCE AND SUPERSEDED/RESCINDED
|
||||
|
||||
* *POLICY ID** **DATE** **TITLE**
|
||||
|
||||
* *NSDM 222** 11 JUN 73 COCOM (S)
|
||||
* *COMMENT** Earlier policy guidelines have been superseded by more recent guidance and have been implemented.
|
||||
|
||||
* *NSDD 11** 22 SEP 81 Munitions/Technology Transfer to the People's Republic of China (S)
|
||||
* *COMMENT** Superseded by Presidential directive after Tiananmen with total ban on such exports still in force.
|
||||
|
||||
* *NSDD 138** 03 APR 84 Combatting Terrorism (U)
|
||||
* *COMMENT** Superseded by NSDD 207.
|
||||
|
||||
* *NSD 15** 22 JUN 89 Open Skies (U)
|
||||
* *COMMENT** Superseded by NSD 73 and Open Skies Treaty.
|
||||
|
||||
* *NSD 29** 30 OCT 89 FY 90 Aqueduct Nuclear Test Program (U)
|
||||
* *COMMENT** Superseded by NSD 68.
|
||||
|
||||
* *NSD 31** 14 NOV 89 Change to FY 1989 and FY 1990 Nuclear Weapons Deployment Plan (C)
|
||||
* *COMMENT** Superseded by NSD 38.
|
||||
|
||||
* *NSD 32** 30 NOV 89 Economic Sanctions against Panama (C)
|
||||
* *COMMENT** Superseded by Operation Just Cause and NSD-34.
|
||||
|
||||
* *NSD 50** 12 OCT 90 Decisions on START and CFE Issues (U)
|
||||
* *COMMENT** Superseded by signed START and CFE treaties.
|
||||
|
||||
* *NSD 52** 22 OCT 90 FY 1991 Sculpin Nuclear Test Program (U)
|
||||
* *COMMENT** Superseded by NSD 68.
|
||||
|
||||
~~TOP SECRET~~
|
||||
Declassify on: OADR
|
||||
UNCLASSIFIED
|
||||
|
||||
---
|
||||
|
||||
<!-- DOCMASTER:PAGE 0003 -->
|
||||
|
||||
## Page 3
|
||||
|
||||
~~TOP SECRET~~
|
||||
|
||||
# UNCLASSIFIED
|
||||
|
||||
NSD 60 30 MAY 91 FY 1992 Nuclear Test Program - Julin (U)
|
||||
|
||||
* *COMMENT** Superseded by NSD 68.
|
||||
|
||||
~~TOP SECRET~~
|
||||
|
||||
* *UNCLASSIFIED**
|
||||
|
||||
---
|
||||
|
||||
<!-- DOCMASTER:PAGE 0004 -->
|
||||
|
||||
## Page 4
|
||||
|
||||
~~TOP SECRET~~
|
||||
* *UNCLASSIFIED**
|
||||
21220
|
||||
|
||||
* *COMPLETED AND NO LONGER IN FORCE**
|
||||
|
||||
* *POLICY ID** **DATE** **TITLE**
|
||||
|
||||
NSAM 299 12 MAY 64 Evacuation and Protection of U.S. Citizens in Danger Areas Abroad (C)
|
||||
* *COMMENT** Responsibilities delegated to Secretaries of State and Defense.
|
||||
|
||||
NSDM 262 29 JUN 74 Use of U.S. Bases in Japan in the Event of Aggression against South Korea (C)
|
||||
|
||||
NSDM 275 10 OCT 74 COCOM Position on Return of Depleted Uranium Tails from USSR (C)
|
||||
* *COMMENT** This is negotiating position for negotiations that are long concluded.
|
||||
|
||||
PD 9 30 MAR 77 Army Special Operations Field Office in Berlin (C)
|
||||
* *COMMENT** Operation shut down in August 1991; files sent to Fort Meade; personnel reassigned.
|
||||
|
||||
NSDD 50 06 AUG 82 Space Assistance and Cooperation Policy (C)
|
||||
* *COMMENT** National space policy and non-proliferation directives should guide this policy.
|
||||
|
||||
NSDD 52 20 AUG 82 Future Political Status of Micronesia Palau (S)
|
||||
* *COMMENT** Compact negotiations were concluded.
|
||||
|
||||
NSDD 158 09 JAN 85 United States Policy in Southeast Asia - The Kampuchea Problem (U)
|
||||
* *COMMENT** Largely completed by Cambodia settlement.
|
||||
|
||||
NSDD 173 10 JUN 85 Building an Interim Framework for Mutual Restraint (S)
|
||||
|
||||
~~TOP SECRET~~
|
||||
Declassify on: OADR
|
||||
* *UNCLASSIFIED**
|
||||
|
||||
---
|
||||
|
||||
<!-- DOCMASTER:PAGE 0005 -->
|
||||
|
||||
## Page 5
|
||||
|
||||
TOP SECRET
|
||||
UNCLASSIFIED
|
||||
2
|
||||
|
||||
NSDD 208 30 JAN 86 United States Policy towards the Southwest Indian Ocean (U)
|
||||
* *COMMENT** Largely irrelevant due to changed circumstances in the region although some aspects remain valid.
|
||||
|
||||
NSDD 238 2 SEP 86 Revised National Security Strategy (TS)
|
||||
|
||||
NSDD 247 10 OCT 86 Ratification of Existing Treaties Limiting Nuclear Testing (U)
|
||||
* *COMMENT** TTBT and PNET treaties signed, ratified and in force.
|
||||
|
||||
NSDD 251 24 DEC 86 Arms Control Discussions (U)
|
||||
* *COMMENT** START treaty negotiated, signed and is in force.
|
||||
|
||||
NSDD 273 07 MAY 87 United States Policy towards South Africa (C)
|
||||
* *COMMENT** Largely accomplished or rendered irrelevant by subsequent developments. However underlying policy remains valid but policy tactics and strategies must be updated.
|
||||
|
||||
NSDD 319 14 NOV 88 U.S. Policy towards Indochina (U)
|
||||
* *COMMENT** Largely completed by Cambodia settlement.
|
||||
|
||||
NSD 3 13 FEB 89 U.S. Policy towards Afghanistan (C)
|
||||
* *COMMENT** Afghan situation warrants fresh look given instability.
|
||||
|
||||
NSD 4 22 FEB 89 U.S. Policy towards May 7, 1989 Elections in Panama (TS/CO)
|
||||
|
||||
NSD 5 18 MAR 89 Legislation to Authorize the Transfer of Funds to the Agency for International Development (AID) for Humanitarian Assistance to Afghanistan (U)
|
||||
* *COMMENT** Legislation submitted.
|
||||
|
||||
TOP SECRET
|
||||
UNCLASSIFIED
|
||||
|
||||
---
|
||||
|
||||
<!-- DOCMASTER:PAGE 0006 -->
|
||||
|
||||
## Page 6
|
||||
|
||||
~~TOP SECRET~~
|
||||
|
||||
# UNCLASSIFIED
|
||||
|
||||
NSD 6 22 MAR 89 Security of U.S. Government Personnel
|
||||
in Panama (C)
|
||||
|
||||
* *COMMENT** Specific to threat to US personnel at time of
|
||||
1989 Panamanian election.
|
||||
|
||||
NSD 8 01 MAY 89 U.S. Policy towards Nicaragua and the
|
||||
Nicaraguan Resistance (C)
|
||||
|
||||
* *COMMENT** Objectives attained with the election and
|
||||
inauguration of democratic government in April 1990.
|
||||
|
||||
NSD 8 11 MAY 89 Sensitive Annex to NSD 8 re U.S. Policy
|
||||
ANNEX towards Nicaragua and the Nicaraguan
|
||||
Resistance (C)
|
||||
|
||||
NSD 9 08 MAY 89 Actions to Respond to the Polish
|
||||
Roundtable Agreement (U)
|
||||
|
||||
NSD 12 06 JUN 89 Lifting the No-Exceptions Policy (U)
|
||||
|
||||
* *COMMENT** Final decision made by COCOM to eliminate
|
||||
the "no exception policy".
|
||||
|
||||
NSD 13 07 JUN 89 Covert Action Annex to NSD-13 on Cocaine
|
||||
ANNEX Trafficking (S)
|
||||
|
||||
NSD 17 22 JUL 89 U.S. Actions in Panama (C)
|
||||
|
||||
* *COMMENT** OBE by Operation Just Cause.
|
||||
|
||||
NSD 21 01 SEP 89 U.S. Policy towards Panama under Noriega
|
||||
after 1 September 1989 (C)
|
||||
|
||||
* *COMMENT** OBE by Operation Just Cause.
|
||||
|
||||
NSD 22 20 SEP 89 United States Policy on Nuclear Testing
|
||||
Arms Control (U)
|
||||
|
||||
* *COMMENT** TTBT and PNET treaties signed, ratified and
|
||||
in force.
|
||||
|
||||
~~TOP SECRET~~
|
||||
|
||||
UNCLASSIFIED
|
||||
|
||||
---
|
||||
|
||||
<!-- DOCMASTER:PAGE 0007 -->
|
||||
|
||||
## Page 7
|
||||
|
||||
~~TOP SECRET~~
|
||||
* *UNCLASSIFIED**
|
||||
4
|
||||
|
||||
NSD 23 22 SEP 89 United States Relations with the Soviet
|
||||
Union (C)
|
||||
|
||||
* *COMMENT** Replaced by policies toward each of the Newly
|
||||
Independent States of the former Soviet Union.
|
||||
|
||||
NSD 25 22 SEP 89 U.S. Policy towards the February 1990
|
||||
Nicaraguan Election (S)
|
||||
|
||||
* *COMMENT** Specific to 1990 election.
|
||||
|
||||
NSD 25 22 SEP 89 [unclear] NSD-25 on U.S. Policy
|
||||
ANNEX towards the February 1990 Nicaraguan
|
||||
Election (S)
|
||||
1.5(c)
|
||||
|
||||
NSD 33 24 JAN 90 U.S. Policy towards Panama - Post
|
||||
Noriega (U)
|
||||
|
||||
* *COMMENT** Most of the specific elements have been
|
||||
undertaken and completed.
|
||||
|
||||
NSD 34 24 JAN 90 Partnership with Panama - Action Plan to
|
||||
Foster Economic Recovery (U)
|
||||
|
||||
* *COMMENT** Most of the specific elements have been
|
||||
undertaken and completed.
|
||||
|
||||
NSD 35 24 JAN 90 U.S. - Soviet Economic Initiatives (C)
|
||||
|
||||
* *COMMENT** Completed, policies toward each of the Newly
|
||||
Independent States have subsequently been developed.
|
||||
|
||||
NSD 36 06 FEB 90 United States Arms Control Policy (U)
|
||||
|
||||
* *COMMENT** START treaty signed, ratified and in force.
|
||||
|
||||
NSD 39 01 MAY 90 COCOM Policy towards Eastern Europe and
|
||||
Soviet Union (C)
|
||||
|
||||
* *COMMENT** Overtaken by events with the dissolution of
|
||||
the former Soviet Union. Changes made by COCOM and
|
||||
implemented in U.S. regulations.
|
||||
|
||||
NSD 40 14 MAY 90 Decisions on START Issues (U)
|
||||
|
||||
~~TOP SECRET~~
|
||||
* *UNCLASSIFIED**
|
||||
|
||||
---
|
||||
|
||||
<!-- DOCMASTER:PAGE 0008 -->
|
||||
|
||||
## Page 8
|
||||
|
||||
~~TOP SECRET~~
|
||||
|
||||
# UNCLASSIFIED
|
||||
|
||||
NSD 45 20 AUG 90 U.S. Policy in Response to the Iraqi Invasion of Kuwait (C)
|
||||
|
||||
* *COMMENT** OBE by operations Desert Shield and Desert Storm.
|
||||
|
||||
NSD 54 15 JAN 91 Responding to Iraqi Aggression in the Gulf (U)
|
||||
|
||||
* *COMMENT** Accomplished by Desert Storm. Findings supersede references to Iraqi leadership.
|
||||
|
||||
NSD 55 26 MAR 91 Change 1 to NSD 48 - Nuclear Weapons Deployment Authorization for FY 1990 and FY 1991 (C)
|
||||
|
||||
* *COMMENT** All nuclear weapons authorized for withdrawal have been withdrawn.
|
||||
|
||||
NSD 59 14 MAY 91 Disposition of Reagan Administration Policy Papers (U)
|
||||
|
||||
~~TOP SECRET~~
|
||||
|
||||
# UNCLASSIFIED
|
||||
22
vendor/autoload.php
vendored
Normal file
22
vendor/autoload.php
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
// autoload.php @generated by Composer
|
||||
|
||||
if (PHP_VERSION_ID < 50600) {
|
||||
if (!headers_sent()) {
|
||||
header('HTTP/1.1 500 Internal Server Error');
|
||||
}
|
||||
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
|
||||
if (!ini_get('display_errors')) {
|
||||
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
|
||||
fwrite(STDERR, $err);
|
||||
} elseif (!headers_sent()) {
|
||||
echo $err;
|
||||
}
|
||||
}
|
||||
throw new RuntimeException($err);
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/composer/autoload_real.php';
|
||||
|
||||
return ComposerAutoloaderInit691f538563ac6695008ddc51b7722c80::getLoader();
|
||||
119
vendor/bin/carbon
vendored
Executable file
119
vendor/bin/carbon
vendored
Executable file
@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Proxy PHP file generated by Composer
|
||||
*
|
||||
* This file includes the referenced bin path (../nesbot/carbon/bin/carbon)
|
||||
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||
*
|
||||
* @generated
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class BinProxyWrapper
|
||||
{
|
||||
private $handle;
|
||||
private $position;
|
||||
private $realpath;
|
||||
|
||||
public function stream_open($path, $mode, $options, &$opened_path)
|
||||
{
|
||||
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||
$opened_path = substr($path, 17);
|
||||
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||
$opened_path = $this->realpath;
|
||||
$this->handle = fopen($this->realpath, $mode);
|
||||
$this->position = 0;
|
||||
|
||||
return (bool) $this->handle;
|
||||
}
|
||||
|
||||
public function stream_read($count)
|
||||
{
|
||||
$data = fread($this->handle, $count);
|
||||
|
||||
if ($this->position === 0) {
|
||||
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||
}
|
||||
|
||||
$this->position += strlen($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function stream_cast($castAs)
|
||||
{
|
||||
return $this->handle;
|
||||
}
|
||||
|
||||
public function stream_close()
|
||||
{
|
||||
fclose($this->handle);
|
||||
}
|
||||
|
||||
public function stream_lock($operation)
|
||||
{
|
||||
return $operation ? flock($this->handle, $operation) : true;
|
||||
}
|
||||
|
||||
public function stream_seek($offset, $whence)
|
||||
{
|
||||
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||
$this->position = ftell($this->handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function stream_eof()
|
||||
{
|
||||
return feof($this->handle);
|
||||
}
|
||||
|
||||
public function stream_stat()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
public function stream_set_option($option, $arg1, $arg2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function url_stat($path, $flags)
|
||||
{
|
||||
$path = substr($path, 17);
|
||||
if (file_exists($path)) {
|
||||
return stat($path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||
) {
|
||||
return include("phpvfscomposer://" . __DIR__ . '/..'.'/nesbot/carbon/bin/carbon');
|
||||
}
|
||||
}
|
||||
|
||||
return include __DIR__ . '/..'.'/nesbot/carbon/bin/carbon';
|
||||
119
vendor/bin/patch-type-declarations
vendored
Executable file
119
vendor/bin/patch-type-declarations
vendored
Executable file
@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Proxy PHP file generated by Composer
|
||||
*
|
||||
* This file includes the referenced bin path (../symfony/error-handler/Resources/bin/patch-type-declarations)
|
||||
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||
*
|
||||
* @generated
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class BinProxyWrapper
|
||||
{
|
||||
private $handle;
|
||||
private $position;
|
||||
private $realpath;
|
||||
|
||||
public function stream_open($path, $mode, $options, &$opened_path)
|
||||
{
|
||||
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||
$opened_path = substr($path, 17);
|
||||
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||
$opened_path = $this->realpath;
|
||||
$this->handle = fopen($this->realpath, $mode);
|
||||
$this->position = 0;
|
||||
|
||||
return (bool) $this->handle;
|
||||
}
|
||||
|
||||
public function stream_read($count)
|
||||
{
|
||||
$data = fread($this->handle, $count);
|
||||
|
||||
if ($this->position === 0) {
|
||||
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||
}
|
||||
|
||||
$this->position += strlen($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function stream_cast($castAs)
|
||||
{
|
||||
return $this->handle;
|
||||
}
|
||||
|
||||
public function stream_close()
|
||||
{
|
||||
fclose($this->handle);
|
||||
}
|
||||
|
||||
public function stream_lock($operation)
|
||||
{
|
||||
return $operation ? flock($this->handle, $operation) : true;
|
||||
}
|
||||
|
||||
public function stream_seek($offset, $whence)
|
||||
{
|
||||
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||
$this->position = ftell($this->handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function stream_eof()
|
||||
{
|
||||
return feof($this->handle);
|
||||
}
|
||||
|
||||
public function stream_stat()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
public function stream_set_option($option, $arg1, $arg2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function url_stat($path, $flags)
|
||||
{
|
||||
$path = substr($path, 17);
|
||||
if (file_exists($path)) {
|
||||
return stat($path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||
) {
|
||||
return include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/error-handler/Resources/bin/patch-type-declarations');
|
||||
}
|
||||
}
|
||||
|
||||
return include __DIR__ . '/..'.'/symfony/error-handler/Resources/bin/patch-type-declarations';
|
||||
119
vendor/bin/var-dump-server
vendored
Executable file
119
vendor/bin/var-dump-server
vendored
Executable file
@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Proxy PHP file generated by Composer
|
||||
*
|
||||
* This file includes the referenced bin path (../symfony/var-dumper/Resources/bin/var-dump-server)
|
||||
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||
*
|
||||
* @generated
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class BinProxyWrapper
|
||||
{
|
||||
private $handle;
|
||||
private $position;
|
||||
private $realpath;
|
||||
|
||||
public function stream_open($path, $mode, $options, &$opened_path)
|
||||
{
|
||||
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||
$opened_path = substr($path, 17);
|
||||
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||
$opened_path = $this->realpath;
|
||||
$this->handle = fopen($this->realpath, $mode);
|
||||
$this->position = 0;
|
||||
|
||||
return (bool) $this->handle;
|
||||
}
|
||||
|
||||
public function stream_read($count)
|
||||
{
|
||||
$data = fread($this->handle, $count);
|
||||
|
||||
if ($this->position === 0) {
|
||||
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||
}
|
||||
|
||||
$this->position += strlen($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function stream_cast($castAs)
|
||||
{
|
||||
return $this->handle;
|
||||
}
|
||||
|
||||
public function stream_close()
|
||||
{
|
||||
fclose($this->handle);
|
||||
}
|
||||
|
||||
public function stream_lock($operation)
|
||||
{
|
||||
return $operation ? flock($this->handle, $operation) : true;
|
||||
}
|
||||
|
||||
public function stream_seek($offset, $whence)
|
||||
{
|
||||
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||
$this->position = ftell($this->handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function stream_eof()
|
||||
{
|
||||
return feof($this->handle);
|
||||
}
|
||||
|
||||
public function stream_stat()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
public function stream_set_option($option, $arg1, $arg2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function url_stat($path, $flags)
|
||||
{
|
||||
$path = substr($path, 17);
|
||||
if (file_exists($path)) {
|
||||
return stat($path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||
) {
|
||||
return include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/var-dumper/Resources/bin/var-dump-server');
|
||||
}
|
||||
}
|
||||
|
||||
return include __DIR__ . '/..'.'/symfony/var-dumper/Resources/bin/var-dump-server';
|
||||
748
vendor/brick/math/CHANGELOG.md
vendored
Normal file
748
vendor/brick/math/CHANGELOG.md
vendored
Normal file
@ -0,0 +1,748 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.17.1](https://github.com/brick/math/releases/tag/0.17.1) - 2026-04-19
|
||||
|
||||
👌 **Improvements**
|
||||
|
||||
- More precise `float` approximations in `BigRational::toFloat()`
|
||||
|
||||
## [0.17.0](https://github.com/brick/math/releases/tag/0.17.0) - 2026-03-17
|
||||
|
||||
💥 **Breaking changes**
|
||||
|
||||
- Deprecated method `BigDecimal::hasNonZeroFractionalPart()` has been removed, use `! $number->getFractionalPart()->isZero()` instead
|
||||
- Exception constructors and factory methods are now `@internal`
|
||||
|
||||
✨ **Compatibility improvements**
|
||||
|
||||
- `BigDecimal::fromFloatExact()` now supports 32-bit PHP
|
||||
|
||||
## [0.16.2](https://github.com/brick/math/releases/tag/0.16.2) - 2026-03-15
|
||||
|
||||
✨ **New features**
|
||||
|
||||
New methods to create a `BigDecimal` from a `float` value:
|
||||
|
||||
- `BigDecimal::fromFloatExact()`
|
||||
- `BigDecimal::fromFloatShortest()`
|
||||
|
||||
## [0.16.1](https://github.com/brick/math/releases/tag/0.16.1) - 2026-03-09
|
||||
|
||||
👌 **Improvements**
|
||||
|
||||
- Add `@return non-empty-string` to `toString()`, `jsonSerialize()` and `__toString()` (#111 by @vudaltsov)
|
||||
|
||||
## [0.16.0](https://github.com/brick/math/releases/tag/0.16.0) - 2026-03-06
|
||||
|
||||
💥 **Breaking changes**
|
||||
|
||||
- **`BigInteger::getLowestSetBit()` now returns `null` instead of `-1` when the number is zero**
|
||||
- Deprecated method `BigRational::simplified()` has been removed, as it is now a no-op
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- New method: `BigDecimal::getIntegralPart()` returns the integral part as `BigInteger` (this method existed with a different signature in version 0.14, and was removed in 0.15)
|
||||
- New method: `BigDecimal::getFractionalPart()` returns the fractional part as `BigDecimal` (this method existed with a different signature and meaning in version 0.14, and was removed in 0.15)
|
||||
|
||||
🗑️ **Deprecations**
|
||||
|
||||
- Method `BigDecimal::hasNonZeroFractionalPart()` is deprecated, use `->getFractionalPart()->isZero()` instead
|
||||
|
||||
## [0.15.0](https://github.com/brick/math/releases/tag/0.15.0) - 2026-02-20
|
||||
|
||||
💥 **Breaking changes**
|
||||
|
||||
- **floating-point inputs are no longer accepted by `of()` and arithmetic methods**, use `of((string) $float)` to get the same behaviour as before (#105)
|
||||
- **`BigRational` is now always simplified to lowest terms:** all operations, including `of()` and `ofFraction()`, now return a fraction in its simplest form (e.g. `2/3` instead of `4/6`)
|
||||
- **`BigDecimal::dividedBy()` now requires the `$scale` parameter**
|
||||
- **`BigInteger::sqrt()` and `BigDecimal::sqrt()` now default to `RoundingMode::Unnecessary`**, explicitly pass `RoundingMode::Down` to get the previous behaviour
|
||||
- **`BigInteger::mod()` now uses Euclidean modulo semantics**: the modulus must be strictly positive, and the result is always non-negative; this change aligns with Java's `BigInteger.mod()` behaviour
|
||||
- **`BigInteger::mod()`, `modInverse()` and `modPow()` now throw `InvalidArgumentException` (instead of `NegativeNumberException`) for negative modulus/exponent arguments**
|
||||
- **`MathException` is now an interface** instead of a class
|
||||
- **`BigDecimal::getPrecision()` now returns `1` for zero values**
|
||||
- `BigNumber::min()`, `max()` and `sum()` now throw an `ArgumentCountError` when called with no arguments (previously threw `InvalidArgumentException`)
|
||||
- `BigInteger::randomBits()` and `randomRange()` now throw `RandomSourceException` when random byte generation fails or returns invalid data
|
||||
|
||||
Deprecated API elements removed:
|
||||
|
||||
- deprecated method `BigInteger::testBit()` has been removed, use `isBitSet()` instead
|
||||
- deprecated method `BigInteger::gcdMultiple()` has been removed, use `gcdAll()` instead
|
||||
- deprecated method `BigDecimal::exactlyDividedBy()` has been removed, use `dividedByExact()` instead
|
||||
- deprecated method `BigDecimal::getIntegralPart()` has been removed (will be re-introduced as returning `BigInteger` in 0.16)
|
||||
- deprecated method `BigDecimal::getFractionalPart()` has been removed (will be re-introduced as returning `BigDecimal` with a different meaning in 0.16)
|
||||
- deprecated method `BigDecimal::stripTrailingZeros()` has been removed, use `strippedOfTrailingZeros()` instead
|
||||
- deprecated method `BigRational::nd()` has been removed, use `ofFraction()` instead
|
||||
- deprecated method `BigRational::quotient()` has been removed, use `getIntegralPart()` instead
|
||||
- deprecated method `BigRational::remainder()` has been removed, use `$number->getNumerator()->remainder($number->getDenominator())` instead
|
||||
- deprecated method `BigRational::quotientAndRemainder()` has been removed, use `$number->getNumerator()->quotientAndRemainder($number->getDenominator())` instead
|
||||
- deprecated `RoundingMode` upper snake case constants (e.g. `HALF_UP`) have been removed, use the pascal case version (e.g. `HalfUp`) instead
|
||||
|
||||
The following breaking changes only affect you if you're using named arguments:
|
||||
|
||||
- `BigInteger::mod()` now uses `$modulus` as the parameter name
|
||||
- `BigInteger::modInverse()` now uses `$modulus` as the parameter name
|
||||
- `BigInteger::modPow()` now uses `$exponent` and `$modulus` as parameter names
|
||||
- `BigInteger::shiftedLeft()` now uses `$bits` as the parameter name
|
||||
- `BigInteger::shiftedRight()` now uses `$bits` as the parameter name
|
||||
- `BigInteger::isBitSet()` now uses `$bitIndex` as the parameter name
|
||||
- `BigInteger::randomBits()` now uses `$bitCount` as the parameter name
|
||||
- `BigDecimal::withPointMovedLeft()` now uses `$places` as the parameter name
|
||||
- `BigDecimal::withPointMovedRight()` now uses `$places` as the parameter name
|
||||
|
||||
The following breaking changes are unlikely to affect you:
|
||||
|
||||
- `DivisionByZeroException::modulusMustNotBeZero()` has been renamed to `zeroModulus()`
|
||||
- `DivisionByZeroException::denominatorMustNotBeZero()` has been renamed to `zeroDenominator()`
|
||||
- `IntegerOverflowException::toIntOverflow()` has been renamed to `integerOutOfRange()`
|
||||
- `RoundingNecessaryException::roundingNecessary()` has been removed
|
||||
|
||||
🗑️ **Deprecations**
|
||||
|
||||
- Method `BigRational::simplified()` is deprecated, as it is now a no-op
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- `BigInteger::power()`, `BigDecimal::power()` and `BigRational::power()` no longer enforce an exponent limit
|
||||
- `BigInteger::shiftedLeft()` and `BigInteger::shiftedRight()` no longer enforce a limit on the number of bits
|
||||
- `BigRational::power()` now accepts negative exponents
|
||||
- New exception: `InvalidArgumentException` for invalid argument errors
|
||||
- New exception: `NoInverseException` for modular inverse errors
|
||||
- New exception: `RandomSourceException` for random source errors
|
||||
|
||||
👌 **Improvements**
|
||||
|
||||
- Narrowed parameter and return types with static analysis annotations (#108 by @simPod)
|
||||
|
||||
## [0.14.8](https://github.com/brick/math/releases/tag/0.14.8) - 2026-02-10
|
||||
|
||||
🗑️ **Deprecations**
|
||||
|
||||
- Method `BigInteger::testBit()` is deprecated, use `isBitSet()` instead
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- New method: `BigInteger::isBitSet()` (replaces `testBit()`)
|
||||
- New method: `BigNumber::toString()` (alias of magic method `__toString()`)
|
||||
|
||||
👌 **Improvements**
|
||||
|
||||
- Performance optimization of `BigRational` comparison methods
|
||||
- More exceptions have been documented with `@throws` annotations
|
||||
|
||||
## [0.14.7](https://github.com/brick/math/releases/tag/0.14.7) - 2026-02-07
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- `clamp()` is now available on the base `BigNumber` class
|
||||
|
||||
👌 **Improvements**
|
||||
|
||||
- Improved `@throws` exception documentation
|
||||
|
||||
## [0.14.6](https://github.com/brick/math/releases/tag/0.14.6) - 2026-02-05
|
||||
|
||||
🗑️ **Deprecations**
|
||||
|
||||
- Not passing a `$scale` to `BigDecimal::dividedBy()` is deprecated; **`$scale` will be required in 0.15**
|
||||
|
||||
👌 **Improvements**
|
||||
|
||||
- `BigRational::toFloat()` never returns `NAN` anymore
|
||||
|
||||
## [0.14.5](https://github.com/brick/math/releases/tag/0.14.5) - 2026-02-03
|
||||
|
||||
🗑️ **Deprecations**
|
||||
|
||||
- Not passing a rounding mode to `BigInteger::sqrt()` and `BigDecimal::sqrt()` triggers a deprecation notice: **the default rounding mode will change from `Down` to `Unnecessary` in 0.15**
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- `BigInteger::sqrt()` and `BigDecimal::sqrt()` now support rounding
|
||||
- `abs()` and `negated()` methods are now available on the base `BigNumber` class
|
||||
|
||||
👌 **Improvements**
|
||||
|
||||
- Alphabet is now checked for duplicate characters in `BigInteger::(from|to)ArbitraryBase()`
|
||||
- `BigNumber::ofNullable()` is now marked as `@pure`
|
||||
|
||||
## [0.14.4](https://github.com/brick/math/releases/tag/0.14.4) - 2026-02-02
|
||||
|
||||
🗑️ **Deprecations**
|
||||
|
||||
- Passing a negative modulus to `BigInteger::mod()` is deprecated to align with Euclidean modulo semantics; it will throw `InvalidArgumentException` in 0.15
|
||||
- Method `BigDecimal::stripTrailingZeros()` is deprecated, use `strippedOfTrailingZeros()` instead
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- `BigInteger::modPow()` now accepts negative bases
|
||||
- New method: `BigDecimal::strippedOfTrailingZeros()` (replaces `stripTrailingZeros()`)
|
||||
|
||||
👌 **Improvements**
|
||||
|
||||
- `clamp()` methods are now marked as `@pure`
|
||||
|
||||
## [0.14.3](https://github.com/brick/math/releases/tag/0.14.3) - 2026-02-01
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- New method: `BigInteger::lcm()`
|
||||
- New method: `BigInteger::lcmAll()`
|
||||
- New method: `BigRational::toRepeatingDecimalString()`
|
||||
|
||||
🐛 **Bug fixes**
|
||||
|
||||
- `BigInteger::gcdAll()` / `gcdMultiple()` could return a negative result when used with a single negative number
|
||||
|
||||
## [0.14.2](https://github.com/brick/math/releases/tag/0.14.2) - 2026-01-30
|
||||
|
||||
🗑️ **Deprecations**
|
||||
|
||||
- **Passing `float` values to `of()` or arithmetic methods is deprecated** and will be removed in 0.15; cast to string explicitly to preserve the previous behaviour (#105)
|
||||
- **Accessing `RoundingMode` enum cases through upper snake case (e.g. `HALF_UP`) is deprecated**, use the pascal case version (e.g. `HalfUp`) instead
|
||||
- Method `BigInteger::gcdMultiple()` is deprecated, use `gcdAll()` instead
|
||||
- Method `BigDecimal::exactlyDividedBy()` is deprecated, use `dividedByExact()` instead
|
||||
- Method `BigDecimal::getIntegralPart()` is deprecated (will be removed in 0.15, and re-introduced as returning `BigInteger` in 0.16)
|
||||
- Method `BigDecimal::getFractionalPart()` is deprecated (will be removed in 0.15, and re-introduced as returning `BigDecimal` with a different meaning in 0.16)
|
||||
- Method `BigRational::nd()` is deprecated, use `ofFraction()` instead
|
||||
- Method `BigRational::quotient()` is deprecated, use `getIntegralPart()` instead
|
||||
- Method `BigRational::remainder()` is deprecated, use `$number->getNumerator()->remainder($number->getDenominator())` instead
|
||||
- Method `BigRational::quotientAndRemainder()` is deprecated, use `$number->getNumerator()->quotientAndRemainder($number->getDenominator())` instead
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- New method: `BigInteger::gcdAll()` (replaces `gcdMultiple()`)
|
||||
- New method: `BigRational::clamp()`
|
||||
- New method: `BigRational::ofFraction()` (replaces `nd()`)
|
||||
- New method: `BigRational::getIntegralPart()` (replaces `quotient()`)
|
||||
- New method: `BigRational::getFractionalPart()`
|
||||
|
||||
👌 **Improvements**
|
||||
|
||||
- All exceptions thrown by the library now implement a common `MathException` interface
|
||||
- `BigInteger::modInverse()` now accepts `BigNumber|int|float|string` instead of just `BigInteger`
|
||||
- `BigInteger::gcdMultiple()` now accepts `BigNumber|int|float|string` instead of just `BigInteger`
|
||||
|
||||
🐛 **Bug fixes**
|
||||
|
||||
- `BigInteger::clamp()` and `BigDecimal::clamp()` now throw an exception on inverted bounds, instead of returning an incorrect result
|
||||
|
||||
## [0.14.1](https://github.com/brick/math/releases/tag/0.14.1) - 2025-11-24
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- New method: `BigNumber::ofNullable()` (#94 by @mrkh995)
|
||||
|
||||
✨ **Compatibility fixes**
|
||||
|
||||
- Fixed warnings on PHP 8.5 (#101 and #102 by @julien-boudry)
|
||||
|
||||
## [0.14.0](https://github.com/brick/math/releases/tag/0.14.0) - 2025-08-29
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- New methods: `BigInteger::clamp()` and `BigDecimal::clamp()` (#96 by @JesterIruka)
|
||||
|
||||
✨ **Improvements**
|
||||
|
||||
- All pure methods in `BigNumber` classes are now marked as `@pure` for better static analysis
|
||||
|
||||
💥 **Breaking changes**
|
||||
|
||||
- Minimum PHP version is now 8.2
|
||||
- `BigNumber` classes are now `readonly`
|
||||
- `BigNumber` is now marked as sealed: it must not be extended outside of this package
|
||||
- Exception classes are now `final`
|
||||
|
||||
## [0.13.1](https://github.com/brick/math/releases/tag/0.13.1) - 2025-03-29
|
||||
|
||||
✨ **Improvements**
|
||||
|
||||
- `__toString()` methods of `BigInteger` and `BigDecimal` are now type-hinted as returning `numeric-string` instead of `string` (#90 by @vudaltsov)
|
||||
|
||||
## [0.13.0](https://github.com/brick/math/releases/tag/0.13.0) - 2025-03-03
|
||||
|
||||
💥 **Breaking changes**
|
||||
|
||||
- `BigDecimal::ofUnscaledValue()` no longer throws an exception if the scale is negative
|
||||
- `MathException` now extends `RuntimeException` instead of `Exception`; this reverts the change introduced in version `0.11.0` (#82)
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- `BigDecimal::ofUnscaledValue()` allows a negative scale (and converts the values to create a zero scale number)
|
||||
|
||||
## [0.12.3](https://github.com/brick/math/releases/tag/0.12.3) - 2025-02-28
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- `BigDecimal::getPrecision()` Returns the number of significant digits in a decimal number
|
||||
|
||||
## [0.12.2](https://github.com/brick/math/releases/tag/0.12.2) - 2025-02-26
|
||||
|
||||
⚡️ **Performance improvements**
|
||||
|
||||
- Division in `NativeCalculator` is now faster for small divisors, thanks to [@Izumi-kun](https://github.com/Izumi-kun) in [#87](https://github.com/brick/math/pull/87).
|
||||
|
||||
👌 **Improvements**
|
||||
|
||||
- Add missing `RoundingNecessaryException` to the `@throws` annotation of `BigNumber::of()`
|
||||
|
||||
## [0.12.1](https://github.com/brick/math/releases/tag/0.12.1) - 2023-11-29
|
||||
|
||||
⚡️ **Performance improvements**
|
||||
|
||||
- `BigNumber::of()` is now faster, thanks to [@SebastienDug](https://github.com/SebastienDug) in [#77](https://github.com/brick/math/pull/77).
|
||||
|
||||
## [0.12.0](https://github.com/brick/math/releases/tag/0.12.0) - 2023-11-26
|
||||
|
||||
💥 **Breaking changes**
|
||||
|
||||
- Minimum PHP version is now 8.1
|
||||
- `RoundingMode` is now an `enum`; if you're type-hinting rounding modes, you need to type-hint against `RoundingMode` instead of `int` now
|
||||
- `BigNumber` classes do not implement the `Serializable` interface anymore (they use the [new custom object serialization mechanism](https://wiki.php.net/rfc/custom_object_serialization))
|
||||
- The following breaking changes only affect you if you're creating your own `BigNumber` subclasses:
|
||||
- the return type of `BigNumber::of()` is now `static`
|
||||
- `BigNumber` has a new abstract method `from()`
|
||||
- all `public` and `protected` functions of `BigNumber` are now `final`
|
||||
|
||||
## [0.11.0](https://github.com/brick/math/releases/tag/0.11.0) - 2023-01-16
|
||||
|
||||
💥 **Breaking changes**
|
||||
|
||||
- Minimum PHP version is now 8.0
|
||||
- Methods accepting a union of types are now strongly typed<sup>*</sup>
|
||||
- `MathException` now extends `Exception` instead of `RuntimeException`
|
||||
|
||||
<sup>* You may now run into type errors if you were passing `Stringable` objects to `of()` or any of the methods
|
||||
internally calling `of()`, with `strict_types` enabled. You can fix this by casting `Stringable` objects to `string`
|
||||
first.</sup>
|
||||
|
||||
## [0.10.2](https://github.com/brick/math/releases/tag/0.10.2) - 2022-08-11
|
||||
|
||||
👌 **Improvements**
|
||||
|
||||
- `BigRational::toFloat()` now simplifies the fraction before performing division (#73) thanks to @olsavmic
|
||||
|
||||
## [0.10.1](https://github.com/brick/math/releases/tag/0.10.1) - 2022-08-02
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- `BigInteger::gcdMultiple()` returns the GCD of multiple `BigInteger` numbers
|
||||
|
||||
## [0.10.0](https://github.com/brick/math/releases/tag/0.10.0) - 2022-06-18
|
||||
|
||||
💥 **Breaking changes**
|
||||
|
||||
- Minimum PHP version is now 7.4
|
||||
|
||||
## [0.9.3](https://github.com/brick/math/releases/tag/0.9.3) - 2021-08-15
|
||||
|
||||
🚀 **Compatibility with PHP 8.1**
|
||||
|
||||
- Support for custom object serialization; this removes a warning on PHP 8.1 due to the `Serializable` interface being deprecated (#60) thanks @TRowbotham
|
||||
|
||||
## [0.9.2](https://github.com/brick/math/releases/tag/0.9.2) - 2021-01-20
|
||||
|
||||
🐛 **Bug fix**
|
||||
|
||||
- Incorrect results could be returned when using the BCMath calculator, with a default scale set with `bcscale()`, on PHP >= 7.2 (#55).
|
||||
|
||||
## [0.9.1](https://github.com/brick/math/releases/tag/0.9.1) - 2020-08-19
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- `BigInteger::not()` returns the bitwise `NOT` value
|
||||
|
||||
🐛 **Bug fixes**
|
||||
|
||||
- `BigInteger::toBytes()` could return an incorrect binary representation for some numbers
|
||||
- The bitwise operations `and()`, `or()`, `xor()` on `BigInteger` could return an incorrect result when the GMP extension is not available
|
||||
|
||||
## [0.9.0](https://github.com/brick/math/releases/tag/0.9.0) - 2020-08-18
|
||||
|
||||
👌 **Improvements**
|
||||
|
||||
- `BigNumber::of()` now accepts `.123` and `123.` formats, both of which return a `BigDecimal`
|
||||
|
||||
💥 **Breaking changes**
|
||||
|
||||
- Deprecated method `BigInteger::powerMod()` has been removed - use `modPow()` instead
|
||||
- Deprecated method `BigInteger::parse()` has been removed - use `fromBase()` instead
|
||||
|
||||
## [0.8.17](https://github.com/brick/math/releases/tag/0.8.17) - 2020-08-19
|
||||
|
||||
🐛 **Bug fix**
|
||||
|
||||
- `BigInteger::toBytes()` could return an incorrect binary representation for some numbers
|
||||
- The bitwise operations `and()`, `or()`, `xor()` on `BigInteger` could return an incorrect result when the GMP extension is not available
|
||||
|
||||
## [0.8.16](https://github.com/brick/math/releases/tag/0.8.16) - 2020-08-18
|
||||
|
||||
🚑 **Critical fix**
|
||||
|
||||
- This version reintroduces the deprecated `BigInteger::parse()` method, that has been removed by mistake in version `0.8.9` and should have lasted for the whole `0.8` release cycle.
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- `BigInteger::modInverse()` calculates a modular multiplicative inverse
|
||||
- `BigInteger::fromBytes()` creates a `BigInteger` from a byte string
|
||||
- `BigInteger::toBytes()` converts a `BigInteger` to a byte string
|
||||
- `BigInteger::randomBits()` creates a pseudo-random `BigInteger` of a given bit length
|
||||
- `BigInteger::randomRange()` creates a pseudo-random `BigInteger` between two bounds
|
||||
|
||||
💩 **Deprecations**
|
||||
|
||||
- `BigInteger::powerMod()` is now deprecated in favour of `modPow()`
|
||||
|
||||
## [0.8.15](https://github.com/brick/math/releases/tag/0.8.15) - 2020-04-15
|
||||
|
||||
🐛 **Fixes**
|
||||
|
||||
- added missing `ext-json` requirement, due to `BigNumber` implementing `JsonSerializable`
|
||||
|
||||
⚡️ **Optimizations**
|
||||
|
||||
- additional optimization in `BigInteger::remainder()`
|
||||
|
||||
## [0.8.14](https://github.com/brick/math/releases/tag/0.8.14) - 2020-02-18
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- `BigInteger::getLowestSetBit()` returns the index of the rightmost one bit
|
||||
|
||||
## [0.8.13](https://github.com/brick/math/releases/tag/0.8.13) - 2020-02-16
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- `BigInteger::isEven()` tests whether the number is even
|
||||
- `BigInteger::isOdd()` tests whether the number is odd
|
||||
- `BigInteger::testBit()` tests if a bit is set
|
||||
- `BigInteger::getBitLength()` returns the number of bits in the minimal representation of the number
|
||||
|
||||
## [0.8.12](https://github.com/brick/math/releases/tag/0.8.12) - 2020-02-03
|
||||
|
||||
🛠️ **Maintenance release**
|
||||
|
||||
Classes are now annotated for better static analysis with [psalm](https://psalm.dev/).
|
||||
|
||||
This is a maintenance release: no bug fixes, no new features, no breaking changes.
|
||||
|
||||
## [0.8.11](https://github.com/brick/math/releases/tag/0.8.11) - 2020-01-23
|
||||
|
||||
✨ **New feature**
|
||||
|
||||
`BigInteger::powerMod()` performs a power-with-modulo operation. Useful for crypto.
|
||||
|
||||
## [0.8.10](https://github.com/brick/math/releases/tag/0.8.10) - 2020-01-21
|
||||
|
||||
✨ **New feature**
|
||||
|
||||
`BigInteger::mod()` returns the **modulo** of two numbers. The *modulo* differs from the *remainder* when the signs of the operands are different.
|
||||
|
||||
## [0.8.9](https://github.com/brick/math/releases/tag/0.8.9) - 2020-01-08
|
||||
|
||||
⚡️ **Performance improvements**
|
||||
|
||||
A few additional optimizations in `BigInteger` and `BigDecimal` when one of the operands can be returned as is. Thanks to @tomtomsen in #24.
|
||||
|
||||
## [0.8.8](https://github.com/brick/math/releases/tag/0.8.8) - 2019-04-25
|
||||
|
||||
🐛 **Bug fixes**
|
||||
|
||||
- `BigInteger::toBase()` could return an empty string for zero values (BCMath & Native calculators only, GMP calculator unaffected)
|
||||
|
||||
✨ **New features**
|
||||
|
||||
- `BigInteger::toArbitraryBase()` converts a number to an arbitrary base, using a custom alphabet
|
||||
- `BigInteger::fromArbitraryBase()` converts a string in an arbitrary base, using a custom alphabet, back to a number
|
||||
|
||||
These methods can be used as the foundation to convert strings between different bases/alphabets, using BigInteger as an intermediate representation.
|
||||
|
||||
💩 **Deprecations**
|
||||
|
||||
- `BigInteger::parse()` is now deprecated in favour of `fromBase()`
|
||||
|
||||
`BigInteger::fromBase()` works the same way as `parse()`, with 2 minor differences:
|
||||
|
||||
- the `$base` parameter is required, it does not default to `10`
|
||||
- it throws a `NumberFormatException` instead of an `InvalidArgumentException` when the number is malformed
|
||||
|
||||
## [0.8.7](https://github.com/brick/math/releases/tag/0.8.7) - 2019-04-20
|
||||
|
||||
**Improvements**
|
||||
|
||||
- Safer conversion from `float` when using custom locales
|
||||
- **Much faster** `NativeCalculator` implementation 🚀
|
||||
|
||||
You can expect **at least a 3x performance improvement** for common arithmetic operations when using the library on systems without GMP or BCMath; it gets exponentially faster on multiplications with a high number of digits. This is due to calculations now being performed on whole blocks of digits (the block size depending on the platform, 32-bit or 64-bit) instead of digit-by-digit as before.
|
||||
|
||||
## [0.8.6](https://github.com/brick/math/releases/tag/0.8.6) - 2019-04-11
|
||||
|
||||
**New method**
|
||||
|
||||
`BigNumber::sum()` returns the sum of one or more numbers.
|
||||
|
||||
## [0.8.5](https://github.com/brick/math/releases/tag/0.8.5) - 2019-02-12
|
||||
|
||||
**Bug fix**: `of()` factory methods could fail when passing a `float` in environments using a `LC_NUMERIC` locale with a decimal separator other than `'.'` (#20).
|
||||
|
||||
Thanks @manowark 👍
|
||||
|
||||
## [0.8.4](https://github.com/brick/math/releases/tag/0.8.4) - 2018-12-07
|
||||
|
||||
**New method**
|
||||
|
||||
`BigDecimal::sqrt()` calculates the square root of a decimal number, to a given scale.
|
||||
|
||||
## [0.8.3](https://github.com/brick/math/releases/tag/0.8.3) - 2018-12-06
|
||||
|
||||
**New method**
|
||||
|
||||
`BigInteger::sqrt()` calculates the square root of a number (thanks @peter279k).
|
||||
|
||||
**New exception**
|
||||
|
||||
`NegativeNumberException` is thrown when calling `sqrt()` on a negative number.
|
||||
|
||||
## [0.8.2](https://github.com/brick/math/releases/tag/0.8.2) - 2018-11-08
|
||||
|
||||
**Performance update**
|
||||
|
||||
- Further improvement of `toInt()` performance
|
||||
- `NativeCalculator` can now perform some multiplications more efficiently
|
||||
|
||||
## [0.8.1](https://github.com/brick/math/releases/tag/0.8.1) - 2018-11-07
|
||||
|
||||
Performance optimization of `toInt()` methods.
|
||||
|
||||
## [0.8.0](https://github.com/brick/math/releases/tag/0.8.0) - 2018-10-13
|
||||
|
||||
**Breaking changes**
|
||||
|
||||
The following deprecated methods have been removed. Use the new method name instead:
|
||||
|
||||
| Method removed | Replacement method |
|
||||
| --- | --- |
|
||||
| `BigDecimal::getIntegral()` | `BigDecimal::getIntegralPart()` |
|
||||
| `BigDecimal::getFraction()` | `BigDecimal::getFractionalPart()` |
|
||||
|
||||
---
|
||||
|
||||
**New features**
|
||||
|
||||
`BigInteger` has been augmented with 5 new methods for bitwise operations:
|
||||
|
||||
| New method | Description |
|
||||
| --- | --- |
|
||||
| `and()` | performs a bitwise `AND` operation on two numbers |
|
||||
| `or()` | performs a bitwise `OR` operation on two numbers |
|
||||
| `xor()` | performs a bitwise `XOR` operation on two numbers |
|
||||
| `shiftedLeft()` | returns the number shifted left by a number of bits |
|
||||
| `shiftedRight()` | returns the number shifted right by a number of bits |
|
||||
|
||||
Thanks to @DASPRiD 👍
|
||||
|
||||
## [0.7.3](https://github.com/brick/math/releases/tag/0.7.3) - 2018-08-20
|
||||
|
||||
**New method:** `BigDecimal::hasNonZeroFractionalPart()`
|
||||
|
||||
**Renamed/deprecated methods:**
|
||||
|
||||
- `BigDecimal::getIntegral()` has been renamed to `getIntegralPart()` and is now deprecated
|
||||
- `BigDecimal::getFraction()` has been renamed to `getFractionalPart()` and is now deprecated
|
||||
|
||||
## [0.7.2](https://github.com/brick/math/releases/tag/0.7.2) - 2018-07-21
|
||||
|
||||
**Performance update**
|
||||
|
||||
`BigInteger::parse()` and `toBase()` now use GMP's built-in base conversion features when available.
|
||||
|
||||
## [0.7.1](https://github.com/brick/math/releases/tag/0.7.1) - 2018-03-01
|
||||
|
||||
This is a maintenance release, no code has been changed.
|
||||
|
||||
- When installed with `--no-dev`, the autoloader does not autoload tests anymore
|
||||
- Tests and other files unnecessary for production are excluded from the dist package
|
||||
|
||||
This will help make installations more compact.
|
||||
|
||||
## [0.7.0](https://github.com/brick/math/releases/tag/0.7.0) - 2017-10-02
|
||||
|
||||
Methods renamed:
|
||||
|
||||
- `BigNumber:sign()` has been renamed to `getSign()`
|
||||
- `BigDecimal::unscaledValue()` has been renamed to `getUnscaledValue()`
|
||||
- `BigDecimal::scale()` has been renamed to `getScale()`
|
||||
- `BigDecimal::integral()` has been renamed to `getIntegral()`
|
||||
- `BigDecimal::fraction()` has been renamed to `getFraction()`
|
||||
- `BigRational::numerator()` has been renamed to `getNumerator()`
|
||||
- `BigRational::denominator()` has been renamed to `getDenominator()`
|
||||
|
||||
Classes renamed:
|
||||
|
||||
- `ArithmeticException` has been renamed to `MathException`
|
||||
|
||||
## [0.6.2](https://github.com/brick/math/releases/tag/0.6.2) - 2017-10-02
|
||||
|
||||
The base class for all exceptions is now `MathException`.
|
||||
`ArithmeticException` has been deprecated, and will be removed in 0.7.0.
|
||||
|
||||
## [0.6.1](https://github.com/brick/math/releases/tag/0.6.1) - 2017-10-02
|
||||
|
||||
A number of methods have been renamed:
|
||||
|
||||
- `BigNumber:sign()` is deprecated; use `getSign()` instead
|
||||
- `BigDecimal::unscaledValue()` is deprecated; use `getUnscaledValue()` instead
|
||||
- `BigDecimal::scale()` is deprecated; use `getScale()` instead
|
||||
- `BigDecimal::integral()` is deprecated; use `getIntegral()` instead
|
||||
- `BigDecimal::fraction()` is deprecated; use `getFraction()` instead
|
||||
- `BigRational::numerator()` is deprecated; use `getNumerator()` instead
|
||||
- `BigRational::denominator()` is deprecated; use `getDenominator()` instead
|
||||
|
||||
The old methods will be removed in version 0.7.0.
|
||||
|
||||
## [0.6.0](https://github.com/brick/math/releases/tag/0.6.0) - 2017-08-25
|
||||
|
||||
- Minimum PHP version is now [7.1](https://gophp71.org/); for PHP 5.6 and PHP 7.0 support, use version `0.5`
|
||||
- Deprecated method `BigDecimal::withScale()` has been removed; use `toScale()` instead
|
||||
- Method `BigNumber::toInteger()` has been renamed to `toInt()`
|
||||
|
||||
## [0.5.4](https://github.com/brick/math/releases/tag/0.5.4) - 2016-10-17
|
||||
|
||||
`BigNumber` classes now implement [JsonSerializable](http://php.net/manual/en/class.jsonserializable.php).
|
||||
The JSON output is always a string.
|
||||
|
||||
## [0.5.3](https://github.com/brick/math/releases/tag/0.5.3) - 2016-03-31
|
||||
|
||||
This is a bugfix release. Dividing by a negative power of 1 with the same scale as the dividend could trigger an incorrect optimization which resulted in a wrong result. See #6.
|
||||
|
||||
## [0.5.2](https://github.com/brick/math/releases/tag/0.5.2) - 2015-08-06
|
||||
|
||||
The `$scale` parameter of `BigDecimal::dividedBy()` is now optional again.
|
||||
|
||||
## [0.5.1](https://github.com/brick/math/releases/tag/0.5.1) - 2015-07-05
|
||||
|
||||
**New method: `BigNumber::toScale()`**
|
||||
|
||||
This allows to convert any `BigNumber` to a `BigDecimal` with a given scale, using rounding if necessary.
|
||||
|
||||
## [0.5.0](https://github.com/brick/math/releases/tag/0.5.0) - 2015-07-04
|
||||
|
||||
**New features**
|
||||
- Common `BigNumber` interface for all classes, with the following methods:
|
||||
- `sign()` and derived methods (`isZero()`, `isPositive()`, ...)
|
||||
- `compareTo()` and derived methods (`isEqualTo()`, `isGreaterThan()`, ...) that work across different `BigNumber` types
|
||||
- `toBigInteger()`, `toBigDecimal()`, `toBigRational`() conversion methods
|
||||
- `toInteger()` and `toFloat()` conversion methods to native types
|
||||
- Unified `of()` behaviour: every class now accepts any type of number, provided that it can be safely converted to the current type
|
||||
- New method: `BigDecimal::exactlyDividedBy()`; this method automatically computes the scale of the result, provided that the division yields a finite number of digits
|
||||
- New methods: `BigRational::quotient()` and `remainder()`
|
||||
- Fine-grained exceptions: `DivisionByZeroException`, `RoundingNecessaryException`, `NumberFormatException`
|
||||
- Factory methods `zero()`, `one()` and `ten()` available in all classes
|
||||
- Rounding mode reintroduced in `BigInteger::dividedBy()`
|
||||
|
||||
This release also comes with many performance improvements.
|
||||
|
||||
---
|
||||
|
||||
**Breaking changes**
|
||||
- `BigInteger`:
|
||||
- `getSign()` is renamed to `sign()`
|
||||
- `toString()` is renamed to `toBase()`
|
||||
- `BigInteger::dividedBy()` now throws an exception by default if the remainder is not zero; use `quotient()` to get the previous behaviour
|
||||
- `BigDecimal`:
|
||||
- `getSign()` is renamed to `sign()`
|
||||
- `getUnscaledValue()` is renamed to `unscaledValue()`
|
||||
- `getScale()` is renamed to `scale()`
|
||||
- `getIntegral()` is renamed to `integral()`
|
||||
- `getFraction()` is renamed to `fraction()`
|
||||
- `divideAndRemainder()` is renamed to `quotientAndRemainder()`
|
||||
- `dividedBy()` now takes a **mandatory** `$scale` parameter **before** the rounding mode
|
||||
- `toBigInteger()` does not accept a `$roundingMode` parameter anymore
|
||||
- `toBigRational()` does not simplify the fraction anymore; explicitly add `->simplified()` to get the previous behaviour
|
||||
- `BigRational`:
|
||||
- `getSign()` is renamed to `sign()`
|
||||
- `getNumerator()` is renamed to `numerator()`
|
||||
- `getDenominator()` is renamed to `denominator()`
|
||||
- `of()` is renamed to `nd()`, while `parse()` is renamed to `of()`
|
||||
- Miscellaneous:
|
||||
- `ArithmeticException` is moved to an `Exception\` sub-namespace
|
||||
- `of()` factory methods now throw `NumberFormatException` instead of `InvalidArgumentException`
|
||||
|
||||
## [0.4.3](https://github.com/brick/math/releases/tag/0.4.3) - 2016-03-31
|
||||
|
||||
Backport of two bug fixes from the 0.5 branch:
|
||||
- `BigInteger::parse()` did not always throw `InvalidArgumentException` as expected
|
||||
- Dividing by a negative power of 1 with the same scale as the dividend could trigger an incorrect optimization which resulted in a wrong result. See #6.
|
||||
|
||||
## [0.4.2](https://github.com/brick/math/releases/tag/0.4.2) - 2015-06-16
|
||||
|
||||
New method: `BigDecimal::stripTrailingZeros()`
|
||||
|
||||
## [0.4.1](https://github.com/brick/math/releases/tag/0.4.1) - 2015-06-12
|
||||
|
||||
Introducing a `BigRational` class, to perform calculations on fractions of any size.
|
||||
|
||||
## [0.4.0](https://github.com/brick/math/releases/tag/0.4.0) - 2015-06-12
|
||||
|
||||
Rounding modes have been removed from `BigInteger`, and are now a concept specific to `BigDecimal`.
|
||||
|
||||
`BigInteger::dividedBy()` now always returns the quotient of the division.
|
||||
|
||||
## [0.3.5](https://github.com/brick/math/releases/tag/0.3.5) - 2016-03-31
|
||||
|
||||
Backport of two bug fixes from the 0.5 branch:
|
||||
|
||||
- `BigInteger::parse()` did not always throw `InvalidArgumentException` as expected
|
||||
- Dividing by a negative power of 1 with the same scale as the dividend could trigger an incorrect optimization which resulted in a wrong result. See #6.
|
||||
|
||||
## [0.3.4](https://github.com/brick/math/releases/tag/0.3.4) - 2015-06-11
|
||||
|
||||
New methods:
|
||||
- `BigInteger::remainder()` returns the remainder of a division only
|
||||
- `BigInteger::gcd()` returns the greatest common divisor of two numbers
|
||||
|
||||
## [0.3.3](https://github.com/brick/math/releases/tag/0.3.3) - 2015-06-07
|
||||
|
||||
Fix `toString()` not handling negative numbers.
|
||||
|
||||
## [0.3.2](https://github.com/brick/math/releases/tag/0.3.2) - 2015-06-07
|
||||
|
||||
`BigInteger` and `BigDecimal` now have a `getSign()` method that returns:
|
||||
- `-1` if the number is negative
|
||||
- `0` if the number is zero
|
||||
- `1` if the number is positive
|
||||
|
||||
## [0.3.1](https://github.com/brick/math/releases/tag/0.3.1) - 2015-06-05
|
||||
|
||||
Minor performance improvements
|
||||
|
||||
## [0.3.0](https://github.com/brick/math/releases/tag/0.3.0) - 2015-06-04
|
||||
|
||||
The `$roundingMode` and `$scale` parameters have been swapped in `BigDecimal::dividedBy()`.
|
||||
|
||||
## [0.2.2](https://github.com/brick/math/releases/tag/0.2.2) - 2015-06-04
|
||||
|
||||
Stronger immutability guarantee for `BigInteger` and `BigDecimal`.
|
||||
|
||||
So far, it would have been possible to break immutability of these classes by calling the `unserialize()` internal function. This release fixes that.
|
||||
|
||||
## [0.2.1](https://github.com/brick/math/releases/tag/0.2.1) - 2015-06-02
|
||||
|
||||
Added `BigDecimal::divideAndRemainder()`
|
||||
|
||||
## [0.2.0](https://github.com/brick/math/releases/tag/0.2.0) - 2015-05-22
|
||||
|
||||
- `min()` and `max()` do not accept an `array` anymore, but a variable number of parameters
|
||||
- **minimum PHP version is now 5.6**
|
||||
- continuous integration with PHP 7
|
||||
|
||||
## [0.1.1](https://github.com/brick/math/releases/tag/0.1.1) - 2014-09-01
|
||||
|
||||
- Added `BigInteger::power()`
|
||||
- Added HHVM support
|
||||
|
||||
## [0.1.0](https://github.com/brick/math/releases/tag/0.1.0) - 2014-08-31
|
||||
|
||||
First beta release.
|
||||
20
vendor/brick/math/LICENSE
vendored
Normal file
20
vendor/brick/math/LICENSE
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-present Benjamin Morel
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
263
vendor/brick/math/README.md
vendored
Normal file
263
vendor/brick/math/README.md
vendored
Normal file
@ -0,0 +1,263 @@
|
||||
## Brick\Math
|
||||
|
||||
<img src="https://raw.githubusercontent.com/brick/brick/master/logo.png" alt="" align="left" height="64">
|
||||
|
||||
A PHP library to work with arbitrary precision numbers.
|
||||
|
||||
[](https://github.com/brick/math/actions)
|
||||
[](https://codecov.io/github/brick/math)
|
||||
[](https://packagist.org/packages/brick/math)
|
||||
[](https://packagist.org/packages/brick/math)
|
||||
[](http://opensource.org/licenses/MIT)
|
||||
|
||||
### Installation
|
||||
|
||||
This library is installable via [Composer](https://getcomposer.org/):
|
||||
|
||||
```bash
|
||||
composer require brick/math
|
||||
```
|
||||
|
||||
### Requirements
|
||||
|
||||
This library requires PHP 8.2 or later.
|
||||
|
||||
For PHP 8.1 compatibility, you can use version `0.13`. For PHP 8.0, you can use version `0.11`. For PHP 7.4, you can use version `0.10`. For PHP 7.1, 7.2 & 7.3, you can use version `0.9`. Note that [PHP versions < 8.1 are EOL](http://php.net/supported-versions.php) and not supported anymore. If you're still using one of these PHP versions, you should consider upgrading as soon as possible.
|
||||
|
||||
Although the library can work seamlessly on any PHP installation, it is highly recommended that you install the
|
||||
[GMP](http://php.net/manual/en/book.gmp.php) or [BCMath](http://php.net/manual/en/book.bc.php) extension
|
||||
to speed up calculations. The fastest available calculator implementation will be automatically selected at runtime.
|
||||
|
||||
### Project status & release process
|
||||
|
||||
While this library is still under development, it is well tested and considered stable enough to use in production
|
||||
environments.
|
||||
|
||||
The current releases are numbered `0.x.y`. When a non-breaking change is introduced (adding new methods, optimizing
|
||||
existing code, etc.), `y` is incremented.
|
||||
|
||||
**When a breaking change is introduced, a new `0.x` version cycle is always started.**
|
||||
|
||||
It is therefore safe to lock your project to a given release cycle, such as `^0.17`.
|
||||
|
||||
If you need to upgrade to a newer release cycle, check the [release history](https://github.com/brick/math/releases)
|
||||
for a list of changes introduced by each further `0.x.0` version.
|
||||
|
||||
### Package contents
|
||||
|
||||
This library provides the following public classes in the `Brick\Math` namespace:
|
||||
|
||||
- [BigNumber](https://github.com/brick/math/blob/0.17.1/src/BigNumber.php): base class for `BigInteger`, `BigDecimal` and `BigRational`
|
||||
- [BigInteger](https://github.com/brick/math/blob/0.17.1/src/BigInteger.php): represents an arbitrary-precision integer number.
|
||||
- [BigDecimal](https://github.com/brick/math/blob/0.17.1/src/BigDecimal.php): represents an arbitrary-precision decimal number.
|
||||
- [BigRational](https://github.com/brick/math/blob/0.17.1/src/BigRational.php): represents an arbitrary-precision rational number (fraction), always reduced to lowest terms.
|
||||
- [RoundingMode](https://github.com/brick/math/blob/0.17.1/src/RoundingMode.php): enum representing all available rounding modes.
|
||||
|
||||
And [exceptions](#exceptions) in the `Brick\Math\Exception` namespace.
|
||||
|
||||
### Overview
|
||||
|
||||
#### Instantiation
|
||||
|
||||
The constructors of the classes are not public, you must use a factory method to obtain an instance.
|
||||
|
||||
All classes provide an `of()` factory method that accepts any of the following types:
|
||||
|
||||
- `BigNumber` instances
|
||||
- `int` numbers
|
||||
- `string` representations of integer, decimal and rational numbers
|
||||
|
||||
Example:
|
||||
|
||||
```php
|
||||
BigInteger::of(123546);
|
||||
BigInteger::of('9999999999999999999999999999999999999999999');
|
||||
|
||||
BigDecimal::of('9.99999999999999999999999999999999999999999999');
|
||||
|
||||
BigRational::of('2/3');
|
||||
```
|
||||
|
||||
Note that all `of()` methods accept all the representations above, *as long as it can be safely converted to
|
||||
the current type*:
|
||||
|
||||
```php
|
||||
BigInteger::of('1.00'); // 1
|
||||
BigInteger::of('1.01'); // RoundingNecessaryException
|
||||
|
||||
BigDecimal::of('1/8'); // 0.125
|
||||
BigDecimal::of('1/3'); // RoundingNecessaryException
|
||||
|
||||
BigRational::of('1.1'); // 11/10
|
||||
BigRational::of('1.15'); // 23/20 (reduced to lowest terms)
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> The `of()` factory method does not accept `float` values, because casting a float to string can be lossy.
|
||||
> To convert a float to a `BigDecimal`, use one of the dedicated methods:
|
||||
>
|
||||
> ```php
|
||||
> // Exact IEEE-754 representation — the value the float actually holds:
|
||||
> BigDecimal::fromFloatExact(0.1); // 0.1000000000000000055511151231257827021181583404541015625
|
||||
>
|
||||
> // Shortest decimal that round-trips back to the same float:
|
||||
> BigDecimal::fromFloatShortest(0.1); // 0.1
|
||||
> ```
|
||||
|
||||
#### Immutability & chaining
|
||||
|
||||
The `BigInteger`, `BigDecimal` and `BigRational` classes are immutable: their value never changes,
|
||||
so that they can be safely passed around. All methods that return a `BigInteger`, `BigDecimal` or `BigRational`
|
||||
return a new object, leaving the original object unaffected:
|
||||
|
||||
```php
|
||||
$ten = BigInteger::of(10);
|
||||
|
||||
echo $ten->plus(5); // 15
|
||||
echo $ten->multipliedBy(3); // 30
|
||||
```
|
||||
|
||||
The methods can be chained for better readability:
|
||||
|
||||
```php
|
||||
echo BigInteger::of(10)->plus(5)->multipliedBy(3); // 45
|
||||
```
|
||||
|
||||
#### Parameter types
|
||||
|
||||
All methods that accept a number: `plus()`, `minus()`, `multipliedBy()`, etc. accept the same types as `of()`.
|
||||
For example, given the following number:
|
||||
|
||||
```php
|
||||
$integer = BigInteger::of(123);
|
||||
```
|
||||
|
||||
The following lines are equivalent:
|
||||
|
||||
```php
|
||||
$integer->multipliedBy(123);
|
||||
$integer->multipliedBy('123');
|
||||
$integer->multipliedBy($integer);
|
||||
```
|
||||
|
||||
Just like `of()`, other types of `BigNumber` are acceptable, as long as they can be safely converted to the current type:
|
||||
|
||||
```php
|
||||
echo BigInteger::of(2)->multipliedBy(BigDecimal::of('2.0')); // 4
|
||||
echo BigInteger::of(2)->multipliedBy(BigDecimal::of('2.5')); // RoundingNecessaryException
|
||||
echo BigDecimal::of(2.5)->multipliedBy(BigInteger::of(2)); // 5.0
|
||||
```
|
||||
|
||||
#### Division & rounding
|
||||
|
||||
##### BigInteger
|
||||
|
||||
By default, dividing a `BigInteger` returns the exact result of the division, or throws an exception if the remainder
|
||||
of the division is not zero:
|
||||
|
||||
```php
|
||||
echo BigInteger::of(999)->dividedBy(3); // 333
|
||||
echo BigInteger::of(1000)->dividedBy(3); // RoundingNecessaryException
|
||||
```
|
||||
|
||||
You can pass an optional [rounding mode](https://github.com/brick/math/blob/0.17.1/src/RoundingMode.php) to round the result, if necessary:
|
||||
|
||||
```php
|
||||
echo BigInteger::of(1000)->dividedBy(3, RoundingMode::Down); // 333
|
||||
echo BigInteger::of(1000)->dividedBy(3, RoundingMode::Up); // 334
|
||||
```
|
||||
|
||||
If you're into quotients and remainders, there are methods for this, too:
|
||||
|
||||
```php
|
||||
echo BigInteger::of(1000)->quotient(3); // 333
|
||||
echo BigInteger::of(1000)->remainder(3); // 1
|
||||
```
|
||||
|
||||
You can even get both at the same time:
|
||||
|
||||
```php
|
||||
[$quotient, $remainder] = BigInteger::of(1000)->quotientAndRemainder(3);
|
||||
```
|
||||
|
||||
##### BigDecimal
|
||||
|
||||
Dividing a `BigDecimal` always requires a scale to be specified. If the exact result of the division does not fit in
|
||||
the given scale, a [rounding mode](https://github.com/brick/math/blob/0.17.1/src/RoundingMode.php) must be provided.
|
||||
|
||||
```php
|
||||
echo BigDecimal::of(1)->dividedBy('8', 3); // 0.125
|
||||
echo BigDecimal::of(1)->dividedBy('8', 2); // RoundingNecessaryException
|
||||
echo BigDecimal::of(1)->dividedBy('8', 2, RoundingMode::HalfDown); // 0.12
|
||||
echo BigDecimal::of(1)->dividedBy('8', 2, RoundingMode::HalfUp); // 0.13
|
||||
```
|
||||
|
||||
If you know that the division yields a finite number of decimals places, you can use `dividedByExact()`, which will
|
||||
automatically compute the required scale to fit the result, or throw an exception if the division yields an infinite
|
||||
repeating decimal:
|
||||
|
||||
```php
|
||||
echo BigDecimal::of(1)->dividedByExact(256); // 0.00390625
|
||||
echo BigDecimal::of(1)->dividedByExact(11); // RoundingNecessaryException
|
||||
```
|
||||
|
||||
##### BigRational
|
||||
|
||||
The result of the division of a `BigRational` can always be represented exactly:
|
||||
|
||||
```php
|
||||
echo BigRational::of('13/99')->dividedBy('7'); // 13/693
|
||||
echo BigRational::of('13/99')->dividedBy('9/8'); // 104/891
|
||||
```
|
||||
|
||||
#### Bitwise operations
|
||||
|
||||
`BigInteger` supports bitwise operations:
|
||||
|
||||
- `and()`
|
||||
- `or()`
|
||||
- `xor()`
|
||||
- `not()`
|
||||
|
||||
and bit shifting:
|
||||
|
||||
- `shiftedLeft()`
|
||||
- `shiftedRight()`
|
||||
|
||||
#### Exceptions
|
||||
|
||||
All exceptions thrown by this library implement the `MathException` interface.
|
||||
This means that you can safely catch all exceptions thrown by this library using a single catch clause:
|
||||
|
||||
```php
|
||||
use Brick\Math\BigDecimal;
|
||||
use Brick\Math\Exception\MathException;
|
||||
|
||||
try {
|
||||
$number = BigInteger::of(1)->dividedBy(3);
|
||||
} catch (MathException $e) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
If you need more granular control over the exceptions thrown, you can catch the specific exception classes:
|
||||
|
||||
- `DivisionByZeroException`
|
||||
- `IntegerOverflowException`
|
||||
- `InvalidArgumentException`
|
||||
- `NegativeNumberException`
|
||||
- `NoInverseException`
|
||||
- `NumberFormatException`
|
||||
- `RoundingNecessaryException`
|
||||
|
||||
#### Serialization
|
||||
|
||||
`BigInteger`, `BigDecimal` and `BigRational` can be safely serialized on a machine and unserialized on another,
|
||||
even if these machines do not share the same set of PHP extensions.
|
||||
|
||||
For example, serializing on a machine with GMP support and unserializing on a machine that does not have this extension
|
||||
installed will still work as expected.
|
||||
|
||||
#### PHPStan extension
|
||||
|
||||
A third-party [PHPStan extension](https://github.com/simPod/phpstan-brick-math) is available for this library. It provides more specific throw type narrowing for brick/math methods, so that PHPStan can infer the exact exception classes thrown. Note that this extension is not maintained by the author of brick/math.
|
||||
14
vendor/brick/math/codecov.yml
vendored
Normal file
14
vendor/brick/math/codecov.yml
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
codecov:
|
||||
require_ci_to_pass: true
|
||||
notify:
|
||||
after_n_builds: 6
|
||||
wait_for_ci: true
|
||||
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
informational: true
|
||||
patch:
|
||||
default:
|
||||
informational: true
|
||||
38
vendor/brick/math/composer.json
vendored
Normal file
38
vendor/brick/math/composer.json
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "brick/math",
|
||||
"description": "Arbitrary-precision arithmetic library",
|
||||
"type": "library",
|
||||
"keywords": [
|
||||
"Brick",
|
||||
"Math",
|
||||
"Mathematics",
|
||||
"Arbitrary-precision",
|
||||
"Arithmetic",
|
||||
"BigInteger",
|
||||
"BigDecimal",
|
||||
"BigRational",
|
||||
"BigNumber",
|
||||
"Bignum",
|
||||
"Decimal",
|
||||
"Rational",
|
||||
"Integer"
|
||||
],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^11.5",
|
||||
"phpstan/phpstan": "2.1.22"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Brick\\Math\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Brick\\Math\\Tests\\": "tests/"
|
||||
}
|
||||
}
|
||||
}
|
||||
1096
vendor/brick/math/src/BigDecimal.php
vendored
Normal file
1096
vendor/brick/math/src/BigDecimal.php
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1388
vendor/brick/math/src/BigInteger.php
vendored
Normal file
1388
vendor/brick/math/src/BigInteger.php
vendored
Normal file
File diff suppressed because it is too large
Load Diff
698
vendor/brick/math/src/BigNumber.php
vendored
Normal file
698
vendor/brick/math/src/BigNumber.php
vendored
Normal file
@ -0,0 +1,698 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math;
|
||||
|
||||
use Brick\Math\Exception\DivisionByZeroException;
|
||||
use Brick\Math\Exception\IntegerOverflowException;
|
||||
use Brick\Math\Exception\InvalidArgumentException;
|
||||
use Brick\Math\Exception\MathException;
|
||||
use Brick\Math\Exception\NumberFormatException;
|
||||
use Brick\Math\Exception\RoundingNecessaryException;
|
||||
use Brick\Math\Internal\Safe;
|
||||
use JsonSerializable;
|
||||
use Override;
|
||||
use Stringable;
|
||||
|
||||
use function assert;
|
||||
use function filter_var;
|
||||
use function is_int;
|
||||
use function is_null;
|
||||
use function ltrim;
|
||||
use function preg_match;
|
||||
use function str_contains;
|
||||
use function str_repeat;
|
||||
use function strlen;
|
||||
use function substr;
|
||||
|
||||
use const FILTER_VALIDATE_INT;
|
||||
use const PREG_UNMATCHED_AS_NULL;
|
||||
|
||||
/**
|
||||
* Base class for arbitrary-precision numbers.
|
||||
*
|
||||
* This class is sealed: it is part of the public API but should not be subclassed in userland.
|
||||
* Protected methods may change in any version.
|
||||
*
|
||||
* @phpstan-sealed BigInteger|BigDecimal|BigRational
|
||||
*/
|
||||
abstract readonly class BigNumber implements JsonSerializable, Stringable
|
||||
{
|
||||
/**
|
||||
* The regular expression used to parse integer or decimal numbers.
|
||||
*/
|
||||
private const PARSE_REGEXP_NUMERICAL =
|
||||
'/^' .
|
||||
'(?<sign>[\-\+])?' .
|
||||
'(?<integral>[0-9]+)?' .
|
||||
'(?<point>\.)?' .
|
||||
'(?<fractional>[0-9]+)?' .
|
||||
'(?:[eE](?<exponent>[\-\+]?[0-9]+))?' .
|
||||
'$/';
|
||||
|
||||
/**
|
||||
* The regular expression used to parse rational numbers.
|
||||
*/
|
||||
private const PARSE_REGEXP_RATIONAL =
|
||||
'/^' .
|
||||
'(?<sign>[\-\+])?' .
|
||||
'(?<numerator>[0-9]+)' .
|
||||
'\/' .
|
||||
'(?<denominator>[0-9]+)' .
|
||||
'$/';
|
||||
|
||||
/**
|
||||
* Creates a BigNumber of the given value.
|
||||
*
|
||||
* When of() is called on BigNumber, the concrete return type is dependent on the given value, with the following
|
||||
* rules:
|
||||
*
|
||||
* - BigNumber instances are returned as is
|
||||
* - integer numbers are returned as BigInteger
|
||||
* - strings containing a `/` character are returned as BigRational
|
||||
* - strings containing a `.` character or using an exponential notation are returned as BigDecimal
|
||||
* - strings containing only digits with an optional leading `+` or `-` sign are returned as BigInteger
|
||||
*
|
||||
* When of() is called on BigInteger, BigDecimal, or BigRational, the resulting number is converted to an instance
|
||||
* of the subclass when possible; otherwise a RoundingNecessaryException exception is thrown.
|
||||
*
|
||||
* @throws NumberFormatException If the format of the number is not valid.
|
||||
* @throws DivisionByZeroException If the value represents a rational number with a denominator of zero.
|
||||
* @throws RoundingNecessaryException If the value cannot be converted to an instance of the subclass without rounding.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public static function of(BigNumber|int|string $value): static
|
||||
{
|
||||
$value = self::_of($value);
|
||||
|
||||
if (static::class === BigNumber::class) {
|
||||
assert($value instanceof static);
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
return static::from($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a BigNumber of the given value, or returns null if the input is null.
|
||||
*
|
||||
* Behaves like of() for non-null values.
|
||||
*
|
||||
* @see BigNumber::of()
|
||||
*
|
||||
* @throws NumberFormatException If the format of the number is not valid.
|
||||
* @throws DivisionByZeroException If the value represents a rational number with a denominator of zero.
|
||||
* @throws RoundingNecessaryException If the value cannot be converted to an instance of the subclass without rounding.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public static function ofNullable(BigNumber|int|string|null $value): ?static
|
||||
{
|
||||
if (is_null($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return static::of($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimum of the given values.
|
||||
*
|
||||
* If several values are equal and minimal, the first one is returned.
|
||||
* This can affect the concrete return type when calling this method on BigNumber.
|
||||
*
|
||||
* @param BigNumber|int|string $a The first number. Must be convertible to an instance of the class this method
|
||||
* is called on.
|
||||
* @param BigNumber|int|string ...$n The additional numbers. Each number must be convertible to an instance of the
|
||||
* class this method is called on.
|
||||
*
|
||||
* @throws MathException If a number is not valid, or is not convertible to an instance of the class this method is
|
||||
* called on.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public static function min(BigNumber|int|string $a, BigNumber|int|string ...$n): static
|
||||
{
|
||||
$min = static::of($a);
|
||||
|
||||
foreach ($n as $value) {
|
||||
$value = static::of($value);
|
||||
|
||||
if ($value->isLessThan($min)) {
|
||||
$min = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $min;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum of the given values.
|
||||
*
|
||||
* If several values are equal and maximal, the first one is returned.
|
||||
* This can affect the concrete return type when calling this method on BigNumber.
|
||||
*
|
||||
* @param BigNumber|int|string $a The first number. Must be convertible to an instance of the class this method
|
||||
* is called on.
|
||||
* @param BigNumber|int|string ...$n The additional numbers. Each number must be convertible to an instance of the
|
||||
* class this method is called on.
|
||||
*
|
||||
* @throws MathException If a number is not valid, or is not convertible to an instance of the class this method is
|
||||
* called on.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public static function max(BigNumber|int|string $a, BigNumber|int|string ...$n): static
|
||||
{
|
||||
$max = static::of($a);
|
||||
|
||||
foreach ($n as $value) {
|
||||
$value = static::of($value);
|
||||
|
||||
if ($value->isGreaterThan($max)) {
|
||||
$max = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sum of the given values.
|
||||
*
|
||||
* When called on BigNumber, sum() accepts any supported type and returns a result whose type is the widest among
|
||||
* the given values (BigInteger < BigDecimal < BigRational).
|
||||
*
|
||||
* When called on BigInteger, BigDecimal, or BigRational, sum() requires that all values can be converted to that
|
||||
* specific subclass, and returns a result of the same type.
|
||||
*
|
||||
* @param BigNumber|int|string $a The first number. Must be convertible to an instance of the class this method
|
||||
* is called on.
|
||||
* @param BigNumber|int|string ...$n The additional numbers. Each number must be convertible to an instance of the
|
||||
* class this method is called on.
|
||||
*
|
||||
* @throws MathException If a number is not valid, or is not convertible to an instance of the class this method is
|
||||
* called on.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public static function sum(BigNumber|int|string $a, BigNumber|int|string ...$n): static
|
||||
{
|
||||
$sum = static::of($a);
|
||||
|
||||
foreach ($n as $value) {
|
||||
$sum = self::add($sum, static::of($value));
|
||||
}
|
||||
|
||||
assert($sum instanceof static);
|
||||
|
||||
return $sum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this number is equal to the given one.
|
||||
*
|
||||
* @throws MathException If the given number is not valid.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public function isEqualTo(BigNumber|int|string $that): bool
|
||||
{
|
||||
return $this->compareTo($that) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this number is strictly less than the given one.
|
||||
*
|
||||
* @throws MathException If the given number is not valid.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public function isLessThan(BigNumber|int|string $that): bool
|
||||
{
|
||||
return $this->compareTo($that) < 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this number is less than or equal to the given one.
|
||||
*
|
||||
* @throws MathException If the given number is not valid.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public function isLessThanOrEqualTo(BigNumber|int|string $that): bool
|
||||
{
|
||||
return $this->compareTo($that) <= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this number is strictly greater than the given one.
|
||||
*
|
||||
* @throws MathException If the given number is not valid.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public function isGreaterThan(BigNumber|int|string $that): bool
|
||||
{
|
||||
return $this->compareTo($that) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this number is greater than or equal to the given one.
|
||||
*
|
||||
* @throws MathException If the given number is not valid.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public function isGreaterThanOrEqualTo(BigNumber|int|string $that): bool
|
||||
{
|
||||
return $this->compareTo($that) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this number equals zero.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public function isZero(): bool
|
||||
{
|
||||
return $this->getSign() === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this number is strictly negative.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public function isNegative(): bool
|
||||
{
|
||||
return $this->getSign() < 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this number is negative or zero.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public function isNegativeOrZero(): bool
|
||||
{
|
||||
return $this->getSign() <= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this number is strictly positive.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public function isPositive(): bool
|
||||
{
|
||||
return $this->getSign() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this number is positive or zero.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public function isPositiveOrZero(): bool
|
||||
{
|
||||
return $this->getSign() >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute value of this number.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public function abs(): static
|
||||
{
|
||||
return $this->isNegative() ? $this->negated() : $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the negated value of this number.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
abstract public function negated(): static;
|
||||
|
||||
/**
|
||||
* Returns the sign of this number.
|
||||
*
|
||||
* Returns -1 if the number is negative, 0 if zero, 1 if positive.
|
||||
*
|
||||
* @return -1|0|1
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
abstract public function getSign(): int;
|
||||
|
||||
/**
|
||||
* Compares this number to the given one.
|
||||
*
|
||||
* Returns -1 if `$this` is lower than, 0 if equal to, 1 if greater than `$that`.
|
||||
*
|
||||
* @return -1|0|1
|
||||
*
|
||||
* @throws MathException If the number is not valid.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
abstract public function compareTo(BigNumber|int|string $that): int;
|
||||
|
||||
/**
|
||||
* Limits (clamps) this number between the given minimum and maximum values.
|
||||
*
|
||||
* If the number is lower than $min, returns $min.
|
||||
* If the number is greater than $max, returns $max.
|
||||
* Otherwise, returns this number unchanged.
|
||||
*
|
||||
* @param BigNumber|int|string $min The minimum. Must be convertible to an instance of the class this method is called on.
|
||||
* @param BigNumber|int|string $max The maximum. Must be convertible to an instance of the class this method is called on.
|
||||
*
|
||||
* @throws MathException If min/max are not convertible to an instance of the class this method is called on.
|
||||
* @throws InvalidArgumentException If min is greater than max.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public function clamp(BigNumber|int|string $min, BigNumber|int|string $max): static
|
||||
{
|
||||
$min = static::of($min);
|
||||
$max = static::of($max);
|
||||
|
||||
if ($min->isGreaterThan($max)) {
|
||||
throw InvalidArgumentException::minGreaterThanMax();
|
||||
}
|
||||
|
||||
if ($this->isLessThan($min)) {
|
||||
return $min;
|
||||
}
|
||||
|
||||
if ($this->isGreaterThan($max)) {
|
||||
return $max;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts this number to a BigInteger.
|
||||
*
|
||||
* @throws RoundingNecessaryException If this number cannot be converted to a BigInteger without rounding.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
abstract public function toBigInteger(): BigInteger;
|
||||
|
||||
/**
|
||||
* Converts this number to a BigDecimal.
|
||||
*
|
||||
* @throws RoundingNecessaryException If this number cannot be converted to a BigDecimal without rounding.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
abstract public function toBigDecimal(): BigDecimal;
|
||||
|
||||
/**
|
||||
* Converts this number to a BigRational.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
abstract public function toBigRational(): BigRational;
|
||||
|
||||
/**
|
||||
* Converts this number to a BigDecimal with the given scale, using rounding if necessary.
|
||||
*
|
||||
* @param non-negative-int $scale The scale of the resulting `BigDecimal`. Must be non-negative.
|
||||
* @param RoundingMode $roundingMode An optional rounding mode, defaults to Unnecessary.
|
||||
*
|
||||
* @throws InvalidArgumentException If the scale is negative.
|
||||
* @throws RoundingNecessaryException If RoundingMode::Unnecessary is used, and this number cannot be converted to
|
||||
* the given scale without rounding.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
abstract public function toScale(int $scale, RoundingMode $roundingMode = RoundingMode::Unnecessary): BigDecimal;
|
||||
|
||||
/**
|
||||
* Returns the exact value of this number as a native integer.
|
||||
*
|
||||
* If this number cannot be converted to a native integer without losing precision, an exception is thrown.
|
||||
* Note that the acceptable range for an integer depends on the platform and differs for 32-bit and 64-bit.
|
||||
*
|
||||
* @throws RoundingNecessaryException If this number cannot be converted to an integer without rounding.
|
||||
* @throws IntegerOverflowException If this number is too large to fit in a native integer.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
abstract public function toInt(): int;
|
||||
|
||||
/**
|
||||
* Returns an approximation of this number as a floating-point value.
|
||||
*
|
||||
* Note that this method can discard information as the precision of a floating-point value
|
||||
* is inherently limited.
|
||||
*
|
||||
* If the number is greater than the largest representable floating point number, positive infinity is returned.
|
||||
* If the number is less than the smallest representable floating point number, negative infinity is returned.
|
||||
* This method never returns NaN.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
abstract public function toFloat(): float;
|
||||
|
||||
/**
|
||||
* Returns a string representation of this number.
|
||||
*
|
||||
* The output of this method can be parsed by the `of()` factory method; this will yield an object equal to this
|
||||
* one, but possibly of a different type if instantiated through `BigNumber::of()`.
|
||||
*
|
||||
* @return non-empty-string
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
abstract public function toString(): string;
|
||||
|
||||
/**
|
||||
* @return non-empty-string
|
||||
*/
|
||||
#[Override]
|
||||
final public function jsonSerialize(): string
|
||||
{
|
||||
return $this->toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-string
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
#[Override]
|
||||
final public function __toString(): string
|
||||
{
|
||||
return $this->toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Overridden by subclasses to convert a BigNumber to an instance of the subclass.
|
||||
*
|
||||
* @throws RoundingNecessaryException If the value cannot be converted.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
abstract protected static function from(BigNumber $number): static;
|
||||
|
||||
/**
|
||||
* Proxy method to access BigInteger's protected constructor from sibling classes.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final protected function newBigInteger(string $value): BigInteger
|
||||
{
|
||||
return new BigInteger($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy method to access BigDecimal's protected constructor from sibling classes.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @param non-negative-int $scale
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final protected function newBigDecimal(string $value, int $scale = 0): BigDecimal
|
||||
{
|
||||
return new BigDecimal($value, $scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy method to access BigRational's protected constructor from sibling classes.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final protected function newBigRational(BigInteger $numerator, BigInteger $denominator, bool $checkDenominator, bool $simplify): BigRational
|
||||
{
|
||||
return new BigRational($numerator, $denominator, $checkDenominator, $simplify);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NumberFormatException If the format of the number is not valid.
|
||||
* @throws DivisionByZeroException If the value represents a rational number with a denominator of zero.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
private static function _of(BigNumber|int|string $value): BigNumber
|
||||
{
|
||||
if ($value instanceof BigNumber) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (is_int($value)) {
|
||||
return new BigInteger((string) $value);
|
||||
}
|
||||
|
||||
if ($value === '') {
|
||||
throw NumberFormatException::emptyNumber();
|
||||
}
|
||||
|
||||
if (str_contains($value, '/')) {
|
||||
// Rational number
|
||||
if (preg_match(self::PARSE_REGEXP_RATIONAL, $value, $matches, PREG_UNMATCHED_AS_NULL) !== 1) {
|
||||
throw NumberFormatException::invalidFormat($value);
|
||||
}
|
||||
|
||||
$sign = $matches['sign'];
|
||||
$numerator = $matches['numerator'];
|
||||
$denominator = $matches['denominator'];
|
||||
|
||||
$numerator = self::cleanUp($sign, $numerator);
|
||||
$denominator = self::cleanUp(null, $denominator);
|
||||
|
||||
if ($denominator === '0') {
|
||||
throw DivisionByZeroException::zeroDenominator();
|
||||
}
|
||||
|
||||
return new BigRational(
|
||||
new BigInteger($numerator),
|
||||
new BigInteger($denominator),
|
||||
false,
|
||||
true,
|
||||
);
|
||||
} else {
|
||||
// Integer or decimal number
|
||||
if (preg_match(self::PARSE_REGEXP_NUMERICAL, $value, $matches, PREG_UNMATCHED_AS_NULL) !== 1) {
|
||||
throw NumberFormatException::invalidFormat($value);
|
||||
}
|
||||
|
||||
$sign = $matches['sign'];
|
||||
$point = $matches['point'];
|
||||
$integral = $matches['integral'];
|
||||
$fractional = $matches['fractional'];
|
||||
$exponent = $matches['exponent'];
|
||||
|
||||
if ($integral === null && $fractional === null) {
|
||||
throw NumberFormatException::invalidFormat($value);
|
||||
}
|
||||
|
||||
if ($integral === null) {
|
||||
$integral = '0';
|
||||
}
|
||||
|
||||
if ($point !== null || $exponent !== null) {
|
||||
$fractional ??= '';
|
||||
|
||||
if ($exponent !== null) {
|
||||
if ($exponent[0] === '-') {
|
||||
$exponent = ltrim(substr($exponent, 1), '0') ?: '0';
|
||||
$exponent = filter_var($exponent, FILTER_VALIDATE_INT);
|
||||
if ($exponent !== false) {
|
||||
$exponent = -$exponent;
|
||||
}
|
||||
} else {
|
||||
if ($exponent[0] === '+') {
|
||||
$exponent = substr($exponent, 1);
|
||||
}
|
||||
$exponent = ltrim($exponent, '0') ?: '0';
|
||||
$exponent = filter_var($exponent, FILTER_VALIDATE_INT);
|
||||
}
|
||||
} else {
|
||||
$exponent = 0;
|
||||
}
|
||||
|
||||
if ($exponent === false) {
|
||||
throw NumberFormatException::exponentTooLarge();
|
||||
}
|
||||
|
||||
$unscaledValue = self::cleanUp($sign, $integral . $fractional);
|
||||
|
||||
$scale = Safe::sub(strlen($fractional), $exponent);
|
||||
|
||||
if ($scale < 0) {
|
||||
if ($unscaledValue !== '0') {
|
||||
$unscaledValue .= str_repeat('0', Safe::neg($scale));
|
||||
}
|
||||
$scale = 0;
|
||||
}
|
||||
|
||||
return new BigDecimal($unscaledValue, $scale);
|
||||
}
|
||||
|
||||
$integral = self::cleanUp($sign, $integral);
|
||||
|
||||
return new BigInteger($integral);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes optional leading zeros and applies sign.
|
||||
*
|
||||
* @param '+'|'-'|null $sign The sign, optional. Null is allowed for convenience and treated as '+'.
|
||||
* @param non-empty-string $number The number, validated as a string of digits.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
private static function cleanUp(string|null $sign, string $number): string
|
||||
{
|
||||
$number = ltrim($number, '0');
|
||||
|
||||
if ($number === '') {
|
||||
return '0';
|
||||
}
|
||||
|
||||
return $sign === '-' ? '-' . $number : $number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds two BigNumber instances in the correct order to avoid a RoundingNecessaryException.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
private static function add(BigNumber $a, BigNumber $b): BigNumber
|
||||
{
|
||||
if ($a instanceof BigRational) {
|
||||
return $a->plus($b);
|
||||
}
|
||||
|
||||
if ($b instanceof BigRational) {
|
||||
return $b->plus($a);
|
||||
}
|
||||
|
||||
if ($a instanceof BigDecimal) {
|
||||
return $a->plus($b);
|
||||
}
|
||||
|
||||
if ($b instanceof BigDecimal) {
|
||||
return $b->plus($a);
|
||||
}
|
||||
|
||||
return $a->plus($b);
|
||||
}
|
||||
}
|
||||
591
vendor/brick/math/src/BigRational.php
vendored
Normal file
591
vendor/brick/math/src/BigRational.php
vendored
Normal file
@ -0,0 +1,591 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math;
|
||||
|
||||
use Brick\Math\Exception\DivisionByZeroException;
|
||||
use Brick\Math\Exception\InvalidArgumentException;
|
||||
use Brick\Math\Exception\MathException;
|
||||
use Brick\Math\Exception\RoundingNecessaryException;
|
||||
use Brick\Math\Internal\DecimalHelper;
|
||||
use Brick\Math\Internal\Safe;
|
||||
use LogicException;
|
||||
use Override;
|
||||
|
||||
use function max;
|
||||
use function min;
|
||||
use function strlen;
|
||||
use function substr;
|
||||
|
||||
/**
|
||||
* An arbitrarily large rational number.
|
||||
*
|
||||
* This class is immutable.
|
||||
*
|
||||
* Fractions are automatically simplified to lowest terms. For example, `2/4` becomes `1/2`.
|
||||
* The denominator is always strictly positive; the sign is carried by the numerator.
|
||||
*/
|
||||
final readonly class BigRational extends BigNumber
|
||||
{
|
||||
/**
|
||||
* The numerator.
|
||||
*/
|
||||
private BigInteger $numerator;
|
||||
|
||||
/**
|
||||
* The denominator. Always strictly positive.
|
||||
*/
|
||||
private BigInteger $denominator;
|
||||
|
||||
/**
|
||||
* Protected constructor. Use a factory method to obtain an instance.
|
||||
*
|
||||
* @param BigInteger $numerator The numerator.
|
||||
* @param BigInteger $denominator The denominator.
|
||||
* @param bool $checkDenominator Whether to check the denominator for negative and zero.
|
||||
*
|
||||
* @throws DivisionByZeroException If the denominator is zero.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
protected function __construct(BigInteger $numerator, BigInteger $denominator, bool $checkDenominator, bool $simplify)
|
||||
{
|
||||
if ($checkDenominator) {
|
||||
if ($denominator->isZero()) {
|
||||
throw DivisionByZeroException::zeroDenominator();
|
||||
}
|
||||
|
||||
if ($denominator->isNegative()) {
|
||||
$numerator = $numerator->negated();
|
||||
$denominator = $denominator->negated();
|
||||
}
|
||||
}
|
||||
|
||||
if ($simplify) {
|
||||
$gcd = $numerator->gcd($denominator);
|
||||
|
||||
$numerator = $numerator->quotient($gcd);
|
||||
$denominator = $denominator->quotient($gcd);
|
||||
}
|
||||
|
||||
$this->numerator = $numerator;
|
||||
$this->denominator = $denominator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a BigRational out of a numerator and a denominator.
|
||||
*
|
||||
* If the denominator is negative, the signs of both the numerator and the denominator
|
||||
* will be inverted to ensure that the denominator is always positive.
|
||||
*
|
||||
* @param BigNumber|int|string $numerator The numerator. Must be convertible to a BigInteger.
|
||||
* @param BigNumber|int|string $denominator The denominator. Must be convertible to a BigInteger.
|
||||
*
|
||||
* @throws MathException If an argument is not valid, or is not convertible to a BigInteger.
|
||||
* @throws DivisionByZeroException If the denominator is zero.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function ofFraction(
|
||||
BigNumber|int|string $numerator,
|
||||
BigNumber|int|string $denominator,
|
||||
): BigRational {
|
||||
$numerator = BigInteger::of($numerator);
|
||||
$denominator = BigInteger::of($denominator);
|
||||
|
||||
return new BigRational($numerator, $denominator, true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a BigRational representing zero.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function zero(): BigRational
|
||||
{
|
||||
/** @var BigRational|null $zero */
|
||||
static $zero;
|
||||
|
||||
if ($zero === null) {
|
||||
$zero = new BigRational(BigInteger::zero(), BigInteger::one(), false, false);
|
||||
}
|
||||
|
||||
return $zero;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a BigRational representing one.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function one(): BigRational
|
||||
{
|
||||
/** @var BigRational|null $one */
|
||||
static $one;
|
||||
|
||||
if ($one === null) {
|
||||
$one = new BigRational(BigInteger::one(), BigInteger::one(), false, false);
|
||||
}
|
||||
|
||||
return $one;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a BigRational representing ten.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function ten(): BigRational
|
||||
{
|
||||
/** @var BigRational|null $ten */
|
||||
static $ten;
|
||||
|
||||
if ($ten === null) {
|
||||
$ten = new BigRational(BigInteger::ten(), BigInteger::one(), false, false);
|
||||
}
|
||||
|
||||
return $ten;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the numerator of this rational number.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function getNumerator(): BigInteger
|
||||
{
|
||||
return $this->numerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the denominator of this rational number.
|
||||
*
|
||||
* The denominator is always strictly positive.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function getDenominator(): BigInteger
|
||||
{
|
||||
return $this->denominator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the integral part of this rational number.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* - `7/3` returns `2` (since 7/3 = 2 + 1/3)
|
||||
* - `-7/3` returns `-2` (since -7/3 = -2 + (-1/3))
|
||||
*
|
||||
* The following identity holds: `$r->isEqualTo($r->getFractionalPart()->plus($r->getIntegralPart()))`. Note that in
|
||||
* this identity, the operand order is significant: the reversed form throws when the fractional part is non-zero.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function getIntegralPart(): BigInteger
|
||||
{
|
||||
return $this->numerator->quotient($this->denominator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the fractional part of this rational number.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* - `7/3` returns `1/3` (since 7/3 = 2 + 1/3)
|
||||
* - `-7/3` returns `-1/3` (since -7/3 = -2 + (-1/3))
|
||||
*
|
||||
* The following identity holds: `$r->isEqualTo($r->getFractionalPart()->plus($r->getIntegralPart()))`. Note that in
|
||||
* this identity, the operand order is significant: the reversed form throws when the fractional part is non-zero.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function getFractionalPart(): BigRational
|
||||
{
|
||||
return new BigRational($this->numerator->remainder($this->denominator), $this->denominator, false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sum of this number and the given one.
|
||||
*
|
||||
* @param BigNumber|int|string $that The number to add.
|
||||
*
|
||||
* @throws MathException If the number is not valid.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function plus(BigNumber|int|string $that): BigRational
|
||||
{
|
||||
$that = BigRational::of($that);
|
||||
|
||||
if ($that->isZero()) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
if ($this->isZero()) {
|
||||
return $that;
|
||||
}
|
||||
|
||||
$numerator = $this->numerator->multipliedBy($that->denominator);
|
||||
$numerator = $numerator->plus($that->numerator->multipliedBy($this->denominator));
|
||||
$denominator = $this->denominator->multipliedBy($that->denominator);
|
||||
|
||||
return new BigRational($numerator, $denominator, false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the difference of this number and the given one.
|
||||
*
|
||||
* @param BigNumber|int|string $that The number to subtract.
|
||||
*
|
||||
* @throws MathException If the number is not valid.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function minus(BigNumber|int|string $that): BigRational
|
||||
{
|
||||
$that = BigRational::of($that);
|
||||
|
||||
if ($that->isZero()) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
if ($this->isZero()) {
|
||||
return $that->negated();
|
||||
}
|
||||
|
||||
$numerator = $this->numerator->multipliedBy($that->denominator);
|
||||
$numerator = $numerator->minus($that->numerator->multipliedBy($this->denominator));
|
||||
$denominator = $this->denominator->multipliedBy($that->denominator);
|
||||
|
||||
return new BigRational($numerator, $denominator, false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the product of this number and the given one.
|
||||
*
|
||||
* @param BigNumber|int|string $that The multiplier.
|
||||
*
|
||||
* @throws MathException If the multiplier is not valid.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function multipliedBy(BigNumber|int|string $that): BigRational
|
||||
{
|
||||
$that = BigRational::of($that);
|
||||
|
||||
if ($that->isZero() || $this->isZero()) {
|
||||
return BigRational::zero();
|
||||
}
|
||||
|
||||
$numerator = $this->numerator->multipliedBy($that->numerator);
|
||||
$denominator = $this->denominator->multipliedBy($that->denominator);
|
||||
|
||||
return new BigRational($numerator, $denominator, false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result of the division of this number by the given one.
|
||||
*
|
||||
* @param BigNumber|int|string $that The divisor.
|
||||
*
|
||||
* @throws MathException If the divisor is not valid.
|
||||
* @throws DivisionByZeroException If the divisor is zero.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function dividedBy(BigNumber|int|string $that): BigRational
|
||||
{
|
||||
$that = BigRational::of($that);
|
||||
|
||||
if ($that->isZero()) {
|
||||
throw DivisionByZeroException::divisionByZero();
|
||||
}
|
||||
|
||||
$numerator = $this->numerator->multipliedBy($that->denominator);
|
||||
$denominator = $this->denominator->multipliedBy($that->numerator);
|
||||
|
||||
return new BigRational($numerator, $denominator, true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this number exponentiated to the given value.
|
||||
*
|
||||
* Unlike BigInteger and BigDecimal, BigRational supports negative exponents:
|
||||
* the result is the reciprocal raised to the absolute value of the exponent.
|
||||
*
|
||||
* @throws DivisionByZeroException If the exponent is negative and this number is zero.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function power(int $exponent): BigRational
|
||||
{
|
||||
if ($exponent === 0) {
|
||||
return BigRational::one();
|
||||
}
|
||||
|
||||
if ($exponent === 1) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
if ($exponent < 0) {
|
||||
if ($this->isZero()) {
|
||||
throw DivisionByZeroException::zeroToNegativePower();
|
||||
}
|
||||
|
||||
return $this->reciprocal()->power(Safe::neg($exponent));
|
||||
}
|
||||
|
||||
return new BigRational(
|
||||
$this->numerator->power($exponent),
|
||||
$this->denominator->power($exponent),
|
||||
false,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the reciprocal of this BigRational.
|
||||
*
|
||||
* The reciprocal has the numerator and denominator swapped.
|
||||
*
|
||||
* @throws DivisionByZeroException If this number is zero.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function reciprocal(): BigRational
|
||||
{
|
||||
if ($this->isZero()) {
|
||||
throw DivisionByZeroException::reciprocalOfZero();
|
||||
}
|
||||
|
||||
return new BigRational($this->denominator, $this->numerator, true, false);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function negated(): static
|
||||
{
|
||||
return new BigRational($this->numerator->negated(), $this->denominator, false, false);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function compareTo(BigNumber|int|string $that): int
|
||||
{
|
||||
$that = BigRational::of($that);
|
||||
|
||||
if ($this->denominator->isEqualTo($that->denominator)) {
|
||||
return $this->numerator->compareTo($that->numerator);
|
||||
}
|
||||
|
||||
return $this->numerator
|
||||
->multipliedBy($that->denominator)
|
||||
->compareTo($that->numerator->multipliedBy($this->denominator));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function getSign(): int
|
||||
{
|
||||
return $this->numerator->getSign();
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function toBigInteger(): BigInteger
|
||||
{
|
||||
if ($this->denominator->isEqualTo(1)) {
|
||||
return $this->numerator;
|
||||
}
|
||||
|
||||
throw RoundingNecessaryException::rationalNotConvertibleToInteger();
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function toBigDecimal(): BigDecimal
|
||||
{
|
||||
$scale = DecimalHelper::computeScaleFromReducedFractionDenominator($this->denominator->toString());
|
||||
|
||||
if ($scale === null) {
|
||||
throw RoundingNecessaryException::rationalNotConvertibleToDecimal();
|
||||
}
|
||||
|
||||
return $this->numerator->toBigDecimal()->dividedBy($this->denominator, $scale)->strippedOfTrailingZeros();
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function toBigRational(): BigRational
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function toScale(int $scale, RoundingMode $roundingMode = RoundingMode::Unnecessary): BigDecimal
|
||||
{
|
||||
if ($scale < 0) { // @phpstan-ignore smaller.alwaysFalse
|
||||
throw InvalidArgumentException::negativeScale();
|
||||
}
|
||||
|
||||
if ($roundingMode === RoundingMode::Unnecessary) {
|
||||
$requiredScale = DecimalHelper::computeScaleFromReducedFractionDenominator($this->denominator->toString());
|
||||
|
||||
if ($requiredScale === null) {
|
||||
throw RoundingNecessaryException::rationalNotConvertibleToDecimal();
|
||||
}
|
||||
|
||||
if ($requiredScale > $scale) {
|
||||
throw RoundingNecessaryException::rationalScaleTooSmall();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->numerator->toBigDecimal()->dividedBy($this->denominator, $scale, $roundingMode);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function toInt(): int
|
||||
{
|
||||
return $this->toBigInteger()->toInt();
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function toFloat(): float
|
||||
{
|
||||
if ($this->denominator->isEqualTo(1)) {
|
||||
return $this->numerator->toFloat();
|
||||
}
|
||||
|
||||
// Avoid $this->numerator->toFloat() / $this->denominator->toFloat(): converting both operands to float first
|
||||
// adds an extra rounding step before the division and can change the final float. Instead, divide in decimal
|
||||
// first and convert the resulting decimal approximation to float once.
|
||||
|
||||
// We need ~17 significant digits for double precision (we use 20 for some margin). Since $scale controls
|
||||
// decimal places (not significant digits), we subtract the estimated order of magnitude so that large results
|
||||
// use fewer decimal places and small results use more (to look past leading zeros). Clamped to [0, 350] as
|
||||
// doubles range from e-324 to e308 (350 ≈ 324 + 20 significant digits + margin).
|
||||
$magnitude = strlen($this->numerator->abs()->toString()) - strlen($this->denominator->toString());
|
||||
$scale = min(350, max(0, 20 - $magnitude));
|
||||
|
||||
$result = $this->numerator
|
||||
->toBigDecimal()
|
||||
->dividedBy($this->denominator, $scale, RoundingMode::HalfEven)
|
||||
->toFloat();
|
||||
|
||||
// Preserve the sign when the decimal approximation underflows to zero.
|
||||
if ($result === 0.0 && $this->numerator->isNegative()) {
|
||||
return -0.0;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function toString(): string
|
||||
{
|
||||
$numerator = $this->numerator->toString();
|
||||
$denominator = $this->denominator->toString();
|
||||
|
||||
if ($denominator === '1') {
|
||||
return $numerator;
|
||||
}
|
||||
|
||||
return $numerator . '/' . $denominator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the decimal representation of this rational number, with repeating decimals in parentheses.
|
||||
*
|
||||
* WARNING: This method is unbounded.
|
||||
* The length of the repeating decimal period can be as large as `denominator - 1`.
|
||||
* For fractions with large denominators, this method can use excessive memory and CPU time.
|
||||
* For example, `1/100019` has a repeating period of 100,018 digits.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* - `10/3` returns `3.(3)`
|
||||
* - `171/70` returns `2.4(428571)`
|
||||
* - `1/2` returns `0.5`
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function toRepeatingDecimalString(): string
|
||||
{
|
||||
if ($this->isZero()) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
$sign = $this->numerator->isNegative() ? '-' : '';
|
||||
$numerator = $this->numerator->abs();
|
||||
$denominator = $this->denominator;
|
||||
|
||||
$integral = $numerator->quotient($denominator);
|
||||
$remainder = $numerator->remainder($denominator);
|
||||
|
||||
$integralString = $integral->toString();
|
||||
|
||||
if ($remainder->isZero()) {
|
||||
return $sign . $integralString;
|
||||
}
|
||||
|
||||
$digits = '';
|
||||
$remainderPositions = [];
|
||||
$index = 0;
|
||||
|
||||
while (! $remainder->isZero()) {
|
||||
$remainderString = $remainder->toString();
|
||||
|
||||
if (isset($remainderPositions[$remainderString])) {
|
||||
$repeatIndex = $remainderPositions[$remainderString];
|
||||
$nonRepeating = substr($digits, 0, $repeatIndex);
|
||||
$repeating = substr($digits, $repeatIndex);
|
||||
|
||||
return $sign . $integralString . '.' . $nonRepeating . '(' . $repeating . ')';
|
||||
}
|
||||
|
||||
$remainderPositions[$remainderString] = $index;
|
||||
$remainder = $remainder->multipliedBy(10);
|
||||
|
||||
$digits .= $remainder->quotient($denominator)->toString();
|
||||
$remainder = $remainder->remainder($denominator);
|
||||
$index++;
|
||||
}
|
||||
|
||||
return $sign . $integralString . '.' . $digits;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is required for serializing the object and SHOULD NOT be accessed directly.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @return array{numerator: BigInteger, denominator: BigInteger}
|
||||
*/
|
||||
public function __serialize(): array
|
||||
{
|
||||
return ['numerator' => $this->numerator, 'denominator' => $this->denominator];
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is only here to allow unserializing the object and cannot be accessed directly.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @param array{numerator: BigInteger, denominator: BigInteger} $data
|
||||
*
|
||||
* @throws LogicException
|
||||
*/
|
||||
public function __unserialize(array $data): void
|
||||
{
|
||||
/** @phpstan-ignore isset.initializedProperty */
|
||||
if (isset($this->numerator)) {
|
||||
throw new LogicException('__unserialize() is an internal function, it must not be called directly.');
|
||||
}
|
||||
|
||||
/** @phpstan-ignore deadCode.unreachable */
|
||||
$this->numerator = $data['numerator'];
|
||||
$this->denominator = $data['denominator'];
|
||||
}
|
||||
|
||||
#[Override]
|
||||
protected static function from(BigNumber $number): static
|
||||
{
|
||||
return $number->toBigRational();
|
||||
}
|
||||
}
|
||||
73
vendor/brick/math/src/Exception/DivisionByZeroException.php
vendored
Normal file
73
vendor/brick/math/src/Exception/DivisionByZeroException.php
vendored
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Exception thrown when a division by zero occurs.
|
||||
*/
|
||||
final class DivisionByZeroException extends RuntimeException implements MathException
|
||||
{
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function __construct(string $message)
|
||||
{
|
||||
parent::__construct($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function divisionByZero(): self
|
||||
{
|
||||
return new self('Division by zero.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function zeroModulus(): self
|
||||
{
|
||||
return new self('The modulus must not be zero.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function zeroDenominator(): self
|
||||
{
|
||||
return new self('The denominator of a rational number must not be zero.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function reciprocalOfZero(): self
|
||||
{
|
||||
return new self('The reciprocal of zero is undefined.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function zeroToNegativePower(): self
|
||||
{
|
||||
return new self('Cannot raise zero to a negative power.');
|
||||
}
|
||||
}
|
||||
56
vendor/brick/math/src/Exception/IntegerOverflowException.php
vendored
Normal file
56
vendor/brick/math/src/Exception/IntegerOverflowException.php
vendored
Normal file
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
use Brick\Math\BigInteger;
|
||||
use RuntimeException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
use const PHP_INT_MAX;
|
||||
use const PHP_INT_MIN;
|
||||
|
||||
/**
|
||||
* Exception thrown when a native integer overflow occurs.
|
||||
*/
|
||||
final class IntegerOverflowException extends RuntimeException implements MathException
|
||||
{
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function __construct(string $message)
|
||||
{
|
||||
parent::__construct($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function integerOutOfRange(BigInteger $value): self
|
||||
{
|
||||
$message = '%s is out of range [%d, %d] and cannot be represented as an integer.';
|
||||
|
||||
return new self(sprintf($message, $value->toString(), PHP_INT_MIN, PHP_INT_MAX));
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function nativeIntegerOverflow(string $expression): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Cannot compute %s because the result is outside the native integer range [%d, %d].',
|
||||
$expression,
|
||||
PHP_INT_MIN,
|
||||
PHP_INT_MAX,
|
||||
));
|
||||
}
|
||||
}
|
||||
123
vendor/brick/math/src/Exception/InvalidArgumentException.php
vendored
Normal file
123
vendor/brick/math/src/Exception/InvalidArgumentException.php
vendored
Normal file
@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* Exception thrown when an invalid argument is provided.
|
||||
*/
|
||||
final class InvalidArgumentException extends \InvalidArgumentException implements MathException
|
||||
{
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function __construct(string $message)
|
||||
{
|
||||
parent::__construct($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function baseOutOfRange(int $base): self
|
||||
{
|
||||
return new self(sprintf('Base %d is out of range [2, 36].', $base));
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function negativeScale(): self
|
||||
{
|
||||
return new self('The scale must not be negative.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function negativeBitIndex(): self
|
||||
{
|
||||
return new self('The bit index must not be negative.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function negativeBitCount(): self
|
||||
{
|
||||
return new self('The bit count must not be negative.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function alphabetTooShort(): self
|
||||
{
|
||||
return new self('The alphabet must contain at least 2 characters.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function duplicateCharsInAlphabet(): self
|
||||
{
|
||||
return new self('The alphabet must not contain duplicate characters.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function minGreaterThanMax(): self
|
||||
{
|
||||
return new self('The minimum value must be less than or equal to the maximum value.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function cannotConvertFloat(string $type): self
|
||||
{
|
||||
return new self(sprintf('Cannot convert %s to a BigDecimal.', $type));
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function negativeExponent(): self
|
||||
{
|
||||
return new self('The exponent must not be negative.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function negativeModulus(): self
|
||||
{
|
||||
return new self('The modulus must not be negative.');
|
||||
}
|
||||
}
|
||||
14
vendor/brick/math/src/Exception/MathException.php
vendored
Normal file
14
vendor/brick/math/src/Exception/MathException.php
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Base interface for all math exceptions.
|
||||
*/
|
||||
interface MathException extends Throwable
|
||||
{
|
||||
}
|
||||
53
vendor/brick/math/src/Exception/NegativeNumberException.php
vendored
Normal file
53
vendor/brick/math/src/Exception/NegativeNumberException.php
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Exception thrown when attempting to perform an unsupported operation, such as a square root, on a negative number.
|
||||
*/
|
||||
final class NegativeNumberException extends RuntimeException implements MathException
|
||||
{
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function __construct(string $message)
|
||||
{
|
||||
parent::__construct($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function squareRootOfNegativeNumber(): self
|
||||
{
|
||||
return new self('Cannot calculate the square root of a negative number.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function toArbitraryBaseOfNegativeNumber(): self
|
||||
{
|
||||
return new self('Cannot convert a negative number to an arbitrary base.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function unsignedBytesOfNegativeNumber(): self
|
||||
{
|
||||
return new self('Cannot convert a negative number to a byte string in unsigned mode.');
|
||||
}
|
||||
}
|
||||
33
vendor/brick/math/src/Exception/NoInverseException.php
vendored
Normal file
33
vendor/brick/math/src/Exception/NoInverseException.php
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Exception thrown when attempting to compute a modular inverse that does not exist.
|
||||
*/
|
||||
final class NoInverseException extends RuntimeException implements MathException
|
||||
{
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function __construct(string $message)
|
||||
{
|
||||
parent::__construct($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function noModularInverse(): self
|
||||
{
|
||||
return new self('This number has no multiplicative inverse modulo the given modulus (they are not coprime).');
|
||||
}
|
||||
}
|
||||
120
vendor/brick/math/src/Exception/NumberFormatException.php
vendored
Normal file
120
vendor/brick/math/src/Exception/NumberFormatException.php
vendored
Normal file
@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
use function dechex;
|
||||
use function ord;
|
||||
use function sprintf;
|
||||
use function strtoupper;
|
||||
|
||||
/**
|
||||
* Exception thrown when attempting to create a number from a string with an invalid format.
|
||||
*/
|
||||
final class NumberFormatException extends RuntimeException implements MathException
|
||||
{
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function __construct(string $message)
|
||||
{
|
||||
parent::__construct($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function invalidFormat(string $value): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Value "%s" does not represent a valid number.',
|
||||
$value,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @param string $char The failing character.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function charNotInAlphabet(string $char): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Character %s is not valid in the given alphabet.',
|
||||
self::charToString($char),
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function charNotValidInBase(string $char, int $base): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Character %s is not valid in base %d.',
|
||||
self::charToString($char),
|
||||
$base,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function emptyNumber(): self
|
||||
{
|
||||
return new self('The number must not be empty.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function emptyByteString(): self
|
||||
{
|
||||
return new self('The byte string must not be empty.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function exponentTooLarge(): self
|
||||
{
|
||||
return new self('The exponent is too large to be represented as an integer.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @pure
|
||||
*/
|
||||
private static function charToString(string $char): string
|
||||
{
|
||||
$ord = ord($char);
|
||||
|
||||
if ($ord < 32 || $ord > 126) {
|
||||
$char = strtoupper(dechex($ord));
|
||||
|
||||
if ($ord < 16) {
|
||||
$char = '0' . $char;
|
||||
}
|
||||
|
||||
return '0x' . $char;
|
||||
}
|
||||
|
||||
return '"' . $char . '"';
|
||||
}
|
||||
}
|
||||
64
vendor/brick/math/src/Exception/RandomSourceException.php
vendored
Normal file
64
vendor/brick/math/src/Exception/RandomSourceException.php
vendored
Normal file
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
use function get_debug_type;
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* Exception thrown when random byte generation fails.
|
||||
*/
|
||||
final class RandomSourceException extends RuntimeException implements MathException
|
||||
{
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function __construct(string $message, ?Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, 0, $previous);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function randomSourceFailure(Throwable $previous): self
|
||||
{
|
||||
return new self('Random byte generation failed.', $previous);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function invalidRandomBytesType(mixed $value): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'The random bytes generator must return a string, got %s.',
|
||||
get_debug_type($value),
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function invalidRandomBytesLength(int $expectedLength, int $actualLength): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'The random bytes generator returned %d byte(s), expected %d.',
|
||||
$actualLength,
|
||||
$expectedLength,
|
||||
));
|
||||
}
|
||||
}
|
||||
133
vendor/brick/math/src/Exception/RoundingNecessaryException.php
vendored
Normal file
133
vendor/brick/math/src/Exception/RoundingNecessaryException.php
vendored
Normal file
@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Exception thrown when a number cannot be represented at the requested scale without rounding.
|
||||
*/
|
||||
final class RoundingNecessaryException extends RuntimeException implements MathException
|
||||
{
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function __construct(string $message)
|
||||
{
|
||||
parent::__construct($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function decimalScaleTooSmall(): self
|
||||
{
|
||||
return new self('This decimal number cannot be represented at the requested scale without rounding.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function rationalScaleTooSmall(): self
|
||||
{
|
||||
return new self('This rational number cannot be represented at the requested scale without rounding.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function integerDivisionNotExact(): self
|
||||
{
|
||||
return new self('The division has a non-zero remainder and cannot be represented as an integer without rounding.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function decimalDivisionNotExact(): self
|
||||
{
|
||||
return new self('The division yields a non-terminating decimal expansion and cannot be represented as a decimal without rounding.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function decimalDivisionScaleTooSmall(): self
|
||||
{
|
||||
return new self('The division result is exact but cannot be represented at the requested scale without rounding.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function integerSquareRootNotExact(): self
|
||||
{
|
||||
return new self('The square root is not exact and cannot be represented as an integer without rounding.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function decimalSquareRootNotExact(): self
|
||||
{
|
||||
return new self('The square root is not exact and cannot be represented as a decimal without rounding.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function decimalSquareRootScaleTooSmall(): self
|
||||
{
|
||||
return new self('The square root is exact but cannot be represented at the requested scale without rounding.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function decimalNotConvertibleToInteger(): self
|
||||
{
|
||||
return new self('This decimal number cannot be represented as an integer without rounding.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function rationalNotConvertibleToInteger(): self
|
||||
{
|
||||
return new self('This rational number cannot be represented as an integer without rounding.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function rationalNotConvertibleToDecimal(): self
|
||||
{
|
||||
return new self('This rational number has a non-terminating decimal expansion and cannot be represented as a decimal without rounding.');
|
||||
}
|
||||
}
|
||||
33
vendor/brick/math/src/Exception/UnsupportedPlatformException.php
vendored
Normal file
33
vendor/brick/math/src/Exception/UnsupportedPlatformException.php
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Exception thrown when the current PHP platform does not support a required feature.
|
||||
*/
|
||||
final class UnsupportedPlatformException extends RuntimeException implements MathException
|
||||
{
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function __construct(string $message)
|
||||
{
|
||||
parent::__construct($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function unsupportedFloatFormat(): self
|
||||
{
|
||||
return new self('Unsupported float format: expected IEEE-754 double.');
|
||||
}
|
||||
}
|
||||
697
vendor/brick/math/src/Internal/Calculator.php
vendored
Normal file
697
vendor/brick/math/src/Internal/Calculator.php
vendored
Normal file
@ -0,0 +1,697 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Internal;
|
||||
|
||||
use Brick\Math\RoundingMode;
|
||||
|
||||
use function chr;
|
||||
use function ltrim;
|
||||
use function ord;
|
||||
use function str_repeat;
|
||||
use function strlen;
|
||||
use function strpos;
|
||||
use function strrev;
|
||||
use function strtolower;
|
||||
use function substr;
|
||||
|
||||
/**
|
||||
* Performs basic operations on arbitrary size integers.
|
||||
*
|
||||
* Unless otherwise specified, all parameters must be validated as non-empty strings of digits,
|
||||
* without leading zero, and with an optional leading minus sign if the number is not zero.
|
||||
*
|
||||
* Any other parameter format will lead to undefined behaviour.
|
||||
* All methods must return strings respecting this format, unless specified otherwise.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
abstract readonly class Calculator
|
||||
{
|
||||
/**
|
||||
* The alphabet for converting from and to base 2 to 36, lowercase.
|
||||
*/
|
||||
public const ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz';
|
||||
|
||||
/**
|
||||
* Returns the absolute value of a number.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public function abs(string $n): string
|
||||
{
|
||||
return ($n[0] === '-') ? substr($n, 1) : $n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Negates a number.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public function neg(string $n): string
|
||||
{
|
||||
if ($n === '0') {
|
||||
return '0';
|
||||
}
|
||||
|
||||
if ($n[0] === '-') {
|
||||
return substr($n, 1);
|
||||
}
|
||||
|
||||
return '-' . $n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two numbers.
|
||||
*
|
||||
* Returns -1 if the first number is less than, 0 if equal to, 1 if greater than the second number.
|
||||
*
|
||||
* @return -1|0|1
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public function cmp(string $a, string $b): int
|
||||
{
|
||||
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
|
||||
|
||||
if ($aNeg && ! $bNeg) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if ($bNeg && ! $aNeg) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
$aLen = strlen($aDig);
|
||||
$bLen = strlen($bDig);
|
||||
|
||||
if ($aLen < $bLen) {
|
||||
$result = -1;
|
||||
} elseif ($aLen > $bLen) {
|
||||
$result = 1;
|
||||
} else {
|
||||
$result = $aDig <=> $bDig;
|
||||
}
|
||||
|
||||
return $aNeg ? -$result : $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds two numbers.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
abstract public function add(string $a, string $b): string;
|
||||
|
||||
/**
|
||||
* Subtracts two numbers.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
abstract public function sub(string $a, string $b): string;
|
||||
|
||||
/**
|
||||
* Multiplies two numbers.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
abstract public function mul(string $a, string $b): string;
|
||||
|
||||
/**
|
||||
* Returns the quotient of the division of two numbers.
|
||||
*
|
||||
* @param string $a The dividend.
|
||||
* @param string $b The divisor, must not be zero.
|
||||
*
|
||||
* @return string The quotient.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
abstract public function divQ(string $a, string $b): string;
|
||||
|
||||
/**
|
||||
* Returns the remainder of the division of two numbers.
|
||||
*
|
||||
* @param string $a The dividend.
|
||||
* @param string $b The divisor, must not be zero.
|
||||
*
|
||||
* @return string The remainder.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
abstract public function divR(string $a, string $b): string;
|
||||
|
||||
/**
|
||||
* Returns the quotient and remainder of the division of two numbers.
|
||||
*
|
||||
* @param string $a The dividend.
|
||||
* @param string $b The divisor, must not be zero.
|
||||
*
|
||||
* @return array{string, string} An array containing the quotient and remainder.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
abstract public function divQR(string $a, string $b): array;
|
||||
|
||||
/**
|
||||
* Exponentiates a number.
|
||||
*
|
||||
* @param string $a The base number.
|
||||
* @param int $e The exponent, validated as a non-negative integer.
|
||||
*
|
||||
* @return string The power.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
abstract public function pow(string $a, int $e): string;
|
||||
|
||||
/**
|
||||
* @param string $b The modulus; must not be zero.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function mod(string $a, string $b): string
|
||||
{
|
||||
return $this->divR($this->add($this->divR($a, $b), $b), $b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the modular multiplicative inverse of $x modulo $m.
|
||||
*
|
||||
* If $x has no multiplicative inverse mod m, this method must return null.
|
||||
*
|
||||
* This method can be overridden by the concrete implementation if the underlying library has built-in support.
|
||||
*
|
||||
* @param string $m The modulus; must not be negative or zero.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function modInverse(string $x, string $m): ?string
|
||||
{
|
||||
if ($m === '1') {
|
||||
return '0';
|
||||
}
|
||||
|
||||
$modVal = $x;
|
||||
|
||||
if ($x[0] === '-' || ($this->cmp($this->abs($x), $m) >= 0)) {
|
||||
$modVal = $this->mod($x, $m);
|
||||
}
|
||||
|
||||
[$g, $x] = $this->gcdExtended($modVal, $m);
|
||||
|
||||
if ($g !== '1') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->mod($this->add($this->mod($x, $m), $m), $m);
|
||||
}
|
||||
|
||||
/**
|
||||
* Raises a number into power with modulo.
|
||||
*
|
||||
* @param string $base The base number.
|
||||
* @param string $exp The exponent; must be positive or zero.
|
||||
* @param string $mod The modulus; must be strictly positive.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
abstract public function modPow(string $base, string $exp, string $mod): string;
|
||||
|
||||
/**
|
||||
* Returns the greatest common divisor of the two numbers.
|
||||
*
|
||||
* This method can be overridden by the concrete implementation if the underlying library
|
||||
* has built-in support for GCD calculations.
|
||||
*
|
||||
* @return string The GCD, always positive, or zero if both arguments are zero.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function gcd(string $a, string $b): string
|
||||
{
|
||||
if ($a === '0') {
|
||||
return $this->abs($b);
|
||||
}
|
||||
|
||||
if ($b === '0') {
|
||||
return $this->abs($a);
|
||||
}
|
||||
|
||||
return $this->gcd($b, $this->divR($a, $b));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the least common multiple of the two numbers.
|
||||
*
|
||||
* This method can be overridden by the concrete implementation if the underlying library
|
||||
* has built-in support for LCM calculations.
|
||||
*
|
||||
* @return string The LCM, always positive, or zero if at least one argument is zero.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function lcm(string $a, string $b): string
|
||||
{
|
||||
if ($a === '0' || $b === '0') {
|
||||
return '0';
|
||||
}
|
||||
|
||||
return $this->divQ($this->abs($this->mul($a, $b)), $this->gcd($a, $b));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the square root of the given number, rounded down.
|
||||
*
|
||||
* The result is the largest x such that x² ≤ n.
|
||||
* The input MUST NOT be negative.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
abstract public function sqrt(string $n): string;
|
||||
|
||||
/**
|
||||
* Converts a number from an arbitrary base.
|
||||
*
|
||||
* This method can be overridden by the concrete implementation if the underlying library
|
||||
* has built-in support for base conversion.
|
||||
*
|
||||
* @param string $number The number, positive or zero, non-empty, case-insensitively validated for the given base.
|
||||
* @param int $base The base of the number, validated from 2 to 36.
|
||||
*
|
||||
* @return string The converted number, following the Calculator conventions.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function fromBase(string $number, int $base): string
|
||||
{
|
||||
return $this->fromArbitraryBase(strtolower($number), self::ALPHABET, $base);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a number to an arbitrary base.
|
||||
*
|
||||
* This method can be overridden by the concrete implementation if the underlying library
|
||||
* has built-in support for base conversion.
|
||||
*
|
||||
* @param string $number The number to convert, following the Calculator conventions.
|
||||
* @param int $base The base to convert to, validated from 2 to 36.
|
||||
*
|
||||
* @return string The converted number, lowercase.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function toBase(string $number, int $base): string
|
||||
{
|
||||
$negative = ($number[0] === '-');
|
||||
|
||||
if ($negative) {
|
||||
$number = substr($number, 1);
|
||||
}
|
||||
|
||||
$number = $this->toArbitraryBase($number, self::ALPHABET, $base);
|
||||
|
||||
if ($negative) {
|
||||
return '-' . $number;
|
||||
}
|
||||
|
||||
return $number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a non-negative number in an arbitrary base using a custom alphabet, to base 10.
|
||||
*
|
||||
* @param string $number The number to convert, validated as a non-empty string,
|
||||
* containing only chars in the given alphabet/base.
|
||||
* @param string $alphabet The alphabet that contains every digit, validated as 2 chars minimum.
|
||||
* @param int $base The base of the number, validated from 2 to alphabet length.
|
||||
*
|
||||
* @return string The number in base 10, following the Calculator conventions.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public function fromArbitraryBase(string $number, string $alphabet, int $base): string
|
||||
{
|
||||
// remove leading "zeros"
|
||||
$number = ltrim($number, $alphabet[0]);
|
||||
|
||||
if ($number === '') {
|
||||
return '0';
|
||||
}
|
||||
|
||||
// optimize for "one"
|
||||
if ($number === $alphabet[1]) {
|
||||
return '1';
|
||||
}
|
||||
|
||||
$result = '0';
|
||||
$power = '1';
|
||||
|
||||
$base = (string) $base;
|
||||
|
||||
for ($i = strlen($number) - 1; $i >= 0; $i--) {
|
||||
$index = strpos($alphabet, $number[$i]);
|
||||
|
||||
if ($index !== 0) {
|
||||
$result = $this->add(
|
||||
$result,
|
||||
($index === 1) ? $power : $this->mul($power, (string) $index),
|
||||
);
|
||||
}
|
||||
|
||||
if ($i !== 0) {
|
||||
$power = $this->mul($power, $base);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a non-negative number to an arbitrary base using a custom alphabet.
|
||||
*
|
||||
* @param string $number The number to convert, positive or zero, following the Calculator conventions.
|
||||
* @param string $alphabet The alphabet that contains every digit, validated as 2 chars minimum.
|
||||
* @param int $base The base to convert to, validated from 2 to alphabet length.
|
||||
*
|
||||
* @return string The converted number in the given alphabet.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public function toArbitraryBase(string $number, string $alphabet, int $base): string
|
||||
{
|
||||
if ($number === '0') {
|
||||
return $alphabet[0];
|
||||
}
|
||||
|
||||
$base = (string) $base;
|
||||
$result = '';
|
||||
|
||||
while ($number !== '0') {
|
||||
[$number, $remainder] = $this->divQR($number, $base);
|
||||
$remainder = (int) $remainder;
|
||||
|
||||
$result .= $alphabet[$remainder];
|
||||
}
|
||||
|
||||
return strrev($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a rounded division.
|
||||
*
|
||||
* When the remainder of the division is not zero, rounding is performed according to the rounding mode provided,
|
||||
* unless RoundingMode::Unnecessary is used, in which case the method returns null.
|
||||
*
|
||||
* @param string $a The dividend.
|
||||
* @param string $b The divisor, must not be zero.
|
||||
* @param RoundingMode $roundingMode The rounding mode.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public function divRound(string $a, string $b, RoundingMode $roundingMode): ?string
|
||||
{
|
||||
[$quotient, $remainder] = $this->divQR($a, $b);
|
||||
|
||||
$hasDiscardedFraction = ($remainder !== '0');
|
||||
$isPositiveOrZero = ($a[0] === '-') === ($b[0] === '-');
|
||||
|
||||
$discardedFractionSign = function () use ($remainder, $b): int {
|
||||
$r = $this->abs($this->mul($remainder, '2'));
|
||||
$b = $this->abs($b);
|
||||
|
||||
return $this->cmp($r, $b);
|
||||
};
|
||||
|
||||
$increment = false;
|
||||
|
||||
switch ($roundingMode) {
|
||||
case RoundingMode::Unnecessary:
|
||||
if ($hasDiscardedFraction) {
|
||||
return null;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case RoundingMode::Up:
|
||||
$increment = $hasDiscardedFraction;
|
||||
|
||||
break;
|
||||
|
||||
case RoundingMode::Down:
|
||||
break;
|
||||
|
||||
case RoundingMode::Ceiling:
|
||||
$increment = $hasDiscardedFraction && $isPositiveOrZero;
|
||||
|
||||
break;
|
||||
|
||||
case RoundingMode::Floor:
|
||||
$increment = $hasDiscardedFraction && ! $isPositiveOrZero;
|
||||
|
||||
break;
|
||||
|
||||
case RoundingMode::HalfUp:
|
||||
$increment = $discardedFractionSign() >= 0;
|
||||
|
||||
break;
|
||||
|
||||
case RoundingMode::HalfDown:
|
||||
$increment = $discardedFractionSign() > 0;
|
||||
|
||||
break;
|
||||
|
||||
case RoundingMode::HalfCeiling:
|
||||
$increment = $isPositiveOrZero ? $discardedFractionSign() >= 0 : $discardedFractionSign() > 0;
|
||||
|
||||
break;
|
||||
|
||||
case RoundingMode::HalfFloor:
|
||||
$increment = $isPositiveOrZero ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0;
|
||||
|
||||
break;
|
||||
|
||||
case RoundingMode::HalfEven:
|
||||
$lastDigit = (int) $quotient[-1];
|
||||
$lastDigitIsEven = ($lastDigit % 2 === 0);
|
||||
$increment = $lastDigitIsEven ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ($increment) {
|
||||
return $this->add($quotient, $isPositiveOrZero ? '1' : '-1');
|
||||
}
|
||||
|
||||
return $quotient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates bitwise AND of two numbers.
|
||||
*
|
||||
* This method can be overridden by the concrete implementation if the underlying library
|
||||
* has built-in support for bitwise operations.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function and(string $a, string $b): string
|
||||
{
|
||||
return $this->bitwise('and', $a, $b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates bitwise OR of two numbers.
|
||||
*
|
||||
* This method can be overridden by the concrete implementation if the underlying library
|
||||
* has built-in support for bitwise operations.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function or(string $a, string $b): string
|
||||
{
|
||||
return $this->bitwise('or', $a, $b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates bitwise XOR of two numbers.
|
||||
*
|
||||
* This method can be overridden by the concrete implementation if the underlying library
|
||||
* has built-in support for bitwise operations.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public function xor(string $a, string $b): string
|
||||
{
|
||||
return $this->bitwise('xor', $a, $b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the sign & digits of the operands.
|
||||
*
|
||||
* @return array{bool, bool, string, string} Whether $a and $b are negative, followed by their digits.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final protected function init(string $a, string $b): array
|
||||
{
|
||||
return [
|
||||
$aNeg = ($a[0] === '-'),
|
||||
$bNeg = ($b[0] === '-'),
|
||||
|
||||
$aNeg ? substr($a, 1) : $a,
|
||||
$bNeg ? substr($b, 1) : $b,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{string, string, string} GCD, X, Y
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
private function gcdExtended(string $a, string $b): array
|
||||
{
|
||||
if ($a === '0') {
|
||||
return [$b, '0', '1'];
|
||||
}
|
||||
|
||||
[$gcd, $x1, $y1] = $this->gcdExtended($this->mod($b, $a), $a);
|
||||
|
||||
$x = $this->sub($y1, $this->mul($this->divQ($b, $a), $x1));
|
||||
$y = $x1;
|
||||
|
||||
return [$gcd, $x, $y];
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a bitwise operation on a decimal number.
|
||||
*
|
||||
* @param 'and'|'or'|'xor' $operator The operator to use.
|
||||
* @param string $a The left operand.
|
||||
* @param string $b The right operand.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
private function bitwise(string $operator, string $a, string $b): string
|
||||
{
|
||||
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
|
||||
|
||||
$aBin = $this->toBinary($aDig);
|
||||
$bBin = $this->toBinary($bDig);
|
||||
|
||||
$aLen = strlen($aBin);
|
||||
$bLen = strlen($bBin);
|
||||
|
||||
if ($aLen > $bLen) {
|
||||
$bBin = str_repeat("\x00", $aLen - $bLen) . $bBin;
|
||||
} elseif ($bLen > $aLen) {
|
||||
$aBin = str_repeat("\x00", $bLen - $aLen) . $aBin;
|
||||
}
|
||||
|
||||
if ($aNeg) {
|
||||
$aBin = $this->twosComplement($aBin);
|
||||
}
|
||||
if ($bNeg) {
|
||||
$bBin = $this->twosComplement($bBin);
|
||||
}
|
||||
|
||||
$value = match ($operator) {
|
||||
'and' => $aBin & $bBin,
|
||||
'or' => $aBin | $bBin,
|
||||
'xor' => $aBin ^ $bBin,
|
||||
};
|
||||
|
||||
$negative = match ($operator) {
|
||||
'and' => $aNeg and $bNeg,
|
||||
'or' => $aNeg or $bNeg,
|
||||
'xor' => $aNeg xor $bNeg,
|
||||
};
|
||||
|
||||
if ($negative) {
|
||||
$value = $this->twosComplement($value);
|
||||
}
|
||||
|
||||
$result = $this->toDecimal($value);
|
||||
|
||||
return $negative ? $this->neg($result) : $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $number A positive, binary number.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
private function twosComplement(string $number): string
|
||||
{
|
||||
$xor = str_repeat("\xff", strlen($number));
|
||||
|
||||
$number ^= $xor;
|
||||
|
||||
for ($i = strlen($number) - 1; $i >= 0; $i--) {
|
||||
$byte = ord($number[$i]);
|
||||
|
||||
if (++$byte !== 256) {
|
||||
$number[$i] = chr($byte);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
$number[$i] = "\x00";
|
||||
|
||||
if ($i === 0) {
|
||||
$number = "\x01" . $number;
|
||||
}
|
||||
}
|
||||
|
||||
return $number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a decimal number to a binary string.
|
||||
*
|
||||
* @param string $number The number to convert, positive or zero, only digits.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
private function toBinary(string $number): string
|
||||
{
|
||||
$result = '';
|
||||
|
||||
while ($number !== '0') {
|
||||
[$number, $remainder] = $this->divQR($number, '256');
|
||||
$result .= chr((int) $remainder);
|
||||
}
|
||||
|
||||
return strrev($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the positive decimal representation of a binary number.
|
||||
*
|
||||
* @param string $bytes The bytes representing the number.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
private function toDecimal(string $bytes): string
|
||||
{
|
||||
$result = '0';
|
||||
$power = '1';
|
||||
|
||||
for ($i = strlen($bytes) - 1; $i >= 0; $i--) {
|
||||
$index = ord($bytes[$i]);
|
||||
|
||||
if ($index !== 0) {
|
||||
$result = $this->add(
|
||||
$result,
|
||||
($index === 1) ? $power : $this->mul($power, (string) $index),
|
||||
);
|
||||
}
|
||||
|
||||
if ($i !== 0) {
|
||||
$power = $this->mul($power, '256');
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
85
vendor/brick/math/src/Internal/Calculator/BcMathCalculator.php
vendored
Normal file
85
vendor/brick/math/src/Internal/Calculator/BcMathCalculator.php
vendored
Normal file
@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Internal\Calculator;
|
||||
|
||||
use Brick\Math\Internal\Calculator;
|
||||
use Override;
|
||||
|
||||
use function bcadd;
|
||||
use function bcdiv;
|
||||
use function bcmod;
|
||||
use function bcmul;
|
||||
use function bcpow;
|
||||
use function bcpowmod;
|
||||
use function bcsqrt;
|
||||
use function bcsub;
|
||||
|
||||
/**
|
||||
* Calculator implementation built around the bcmath library.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final readonly class BcMathCalculator extends Calculator
|
||||
{
|
||||
#[Override]
|
||||
public function add(string $a, string $b): string
|
||||
{
|
||||
return bcadd($a, $b, 0);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function sub(string $a, string $b): string
|
||||
{
|
||||
return bcsub($a, $b, 0);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function mul(string $a, string $b): string
|
||||
{
|
||||
return bcmul($a, $b, 0);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function divQ(string $a, string $b): string
|
||||
{
|
||||
return bcdiv($a, $b, 0);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function divR(string $a, string $b): string
|
||||
{
|
||||
return bcmod($a, $b, 0);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function divQR(string $a, string $b): array
|
||||
{
|
||||
$q = bcdiv($a, $b, 0);
|
||||
$r = bcmod($a, $b, 0);
|
||||
|
||||
return [$q, $r];
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function pow(string $a, int $e): string
|
||||
{
|
||||
return bcpow($a, (string) $e, 0);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function modPow(string $base, string $exp, string $mod): string
|
||||
{
|
||||
// normalize to Euclidean representative so modPow() stays consistent with mod()
|
||||
$base = $this->mod($base, $mod);
|
||||
|
||||
return bcpowmod($base, $exp, $mod, 0);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function sqrt(string $n): string
|
||||
{
|
||||
return bcsqrt($n, 0);
|
||||
}
|
||||
}
|
||||
152
vendor/brick/math/src/Internal/Calculator/GmpCalculator.php
vendored
Normal file
152
vendor/brick/math/src/Internal/Calculator/GmpCalculator.php
vendored
Normal file
@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Internal\Calculator;
|
||||
|
||||
use Brick\Math\Internal\Calculator;
|
||||
use GMP;
|
||||
use Override;
|
||||
|
||||
use function gmp_add;
|
||||
use function gmp_and;
|
||||
use function gmp_div_q;
|
||||
use function gmp_div_qr;
|
||||
use function gmp_div_r;
|
||||
use function gmp_gcd;
|
||||
use function gmp_init;
|
||||
use function gmp_invert;
|
||||
use function gmp_lcm;
|
||||
use function gmp_mul;
|
||||
use function gmp_or;
|
||||
use function gmp_pow;
|
||||
use function gmp_powm;
|
||||
use function gmp_sqrt;
|
||||
use function gmp_strval;
|
||||
use function gmp_sub;
|
||||
use function gmp_xor;
|
||||
|
||||
/**
|
||||
* Calculator implementation built around the GMP library.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final readonly class GmpCalculator extends Calculator
|
||||
{
|
||||
#[Override]
|
||||
public function add(string $a, string $b): string
|
||||
{
|
||||
return gmp_strval(gmp_add($a, $b));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function sub(string $a, string $b): string
|
||||
{
|
||||
return gmp_strval(gmp_sub($a, $b));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function mul(string $a, string $b): string
|
||||
{
|
||||
return gmp_strval(gmp_mul($a, $b));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function divQ(string $a, string $b): string
|
||||
{
|
||||
return gmp_strval(gmp_div_q($a, $b));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function divR(string $a, string $b): string
|
||||
{
|
||||
return gmp_strval(gmp_div_r($a, $b));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function divQR(string $a, string $b): array
|
||||
{
|
||||
[$q, $r] = gmp_div_qr($a, $b);
|
||||
|
||||
/**
|
||||
* @var GMP $q
|
||||
* @var GMP $r
|
||||
*/
|
||||
return [
|
||||
gmp_strval($q),
|
||||
gmp_strval($r),
|
||||
];
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function pow(string $a, int $e): string
|
||||
{
|
||||
return gmp_strval(gmp_pow($a, $e));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function modInverse(string $x, string $m): ?string
|
||||
{
|
||||
$result = gmp_invert($x, $m);
|
||||
|
||||
if ($result === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return gmp_strval($result);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function modPow(string $base, string $exp, string $mod): string
|
||||
{
|
||||
return gmp_strval(gmp_powm($base, $exp, $mod));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function gcd(string $a, string $b): string
|
||||
{
|
||||
return gmp_strval(gmp_gcd($a, $b));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function lcm(string $a, string $b): string
|
||||
{
|
||||
return gmp_strval(gmp_lcm($a, $b));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function fromBase(string $number, int $base): string
|
||||
{
|
||||
return gmp_strval(gmp_init($number, $base));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function toBase(string $number, int $base): string
|
||||
{
|
||||
return gmp_strval($number, $base);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function and(string $a, string $b): string
|
||||
{
|
||||
return gmp_strval(gmp_and($a, $b));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function or(string $a, string $b): string
|
||||
{
|
||||
return gmp_strval(gmp_or($a, $b));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function xor(string $a, string $b): string
|
||||
{
|
||||
return gmp_strval(gmp_xor($a, $b));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function sqrt(string $n): string
|
||||
{
|
||||
return gmp_strval(gmp_sqrt($n));
|
||||
}
|
||||
}
|
||||
616
vendor/brick/math/src/Internal/Calculator/NativeCalculator.php
vendored
Normal file
616
vendor/brick/math/src/Internal/Calculator/NativeCalculator.php
vendored
Normal file
@ -0,0 +1,616 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Internal\Calculator;
|
||||
|
||||
use Brick\Math\Internal\Calculator;
|
||||
use Override;
|
||||
|
||||
use function assert;
|
||||
use function in_array;
|
||||
use function intdiv;
|
||||
use function is_int;
|
||||
use function ltrim;
|
||||
use function str_pad;
|
||||
use function str_repeat;
|
||||
use function strcmp;
|
||||
use function strlen;
|
||||
use function substr;
|
||||
|
||||
use const PHP_INT_SIZE;
|
||||
use const STR_PAD_LEFT;
|
||||
|
||||
/**
|
||||
* Calculator implementation using only native PHP code.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final readonly class NativeCalculator extends Calculator
|
||||
{
|
||||
/**
|
||||
* The max number of digits the platform can natively add, subtract, multiply or divide without overflow.
|
||||
* For multiplication, this represents the max sum of the lengths of both operands.
|
||||
*
|
||||
* In addition, it is assumed that an extra digit can hold a carry (1) without overflowing.
|
||||
* Example: 32-bit: max number 1,999,999,999 (9 digits + carry)
|
||||
* 64-bit: max number 1,999,999,999,999,999,999 (18 digits + carry)
|
||||
*/
|
||||
private int $maxDigits;
|
||||
|
||||
/**
|
||||
* @pure
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->maxDigits = match (PHP_INT_SIZE) {
|
||||
4 => 9,
|
||||
8 => 18,
|
||||
};
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function add(string $a, string $b): string
|
||||
{
|
||||
/**
|
||||
* @var numeric-string $a
|
||||
* @var numeric-string $b
|
||||
*/
|
||||
$result = $a + $b;
|
||||
|
||||
if (is_int($result)) {
|
||||
return (string) $result;
|
||||
}
|
||||
|
||||
if ($a === '0') {
|
||||
return $b;
|
||||
}
|
||||
|
||||
if ($b === '0') {
|
||||
return $a;
|
||||
}
|
||||
|
||||
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
|
||||
|
||||
$result = $aNeg === $bNeg ? $this->doAdd($aDig, $bDig) : $this->doSub($aDig, $bDig);
|
||||
|
||||
if ($aNeg) {
|
||||
$result = $this->neg($result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function sub(string $a, string $b): string
|
||||
{
|
||||
return $this->add($a, $this->neg($b));
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function mul(string $a, string $b): string
|
||||
{
|
||||
/**
|
||||
* @var numeric-string $a
|
||||
* @var numeric-string $b
|
||||
*/
|
||||
$result = $a * $b;
|
||||
|
||||
if (is_int($result)) {
|
||||
return (string) $result;
|
||||
}
|
||||
|
||||
if ($a === '0' || $b === '0') {
|
||||
return '0';
|
||||
}
|
||||
|
||||
if ($a === '1') {
|
||||
return $b;
|
||||
}
|
||||
|
||||
if ($b === '1') {
|
||||
return $a;
|
||||
}
|
||||
|
||||
if ($a === '-1') {
|
||||
return $this->neg($b);
|
||||
}
|
||||
|
||||
if ($b === '-1') {
|
||||
return $this->neg($a);
|
||||
}
|
||||
|
||||
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
|
||||
|
||||
$result = $this->doMul($aDig, $bDig);
|
||||
|
||||
if ($aNeg !== $bNeg) {
|
||||
$result = $this->neg($result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function divQ(string $a, string $b): string
|
||||
{
|
||||
return $this->divQR($a, $b)[0];
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function divR(string $a, string $b): string
|
||||
{
|
||||
return $this->divQR($a, $b)[1];
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function divQR(string $a, string $b): array
|
||||
{
|
||||
if ($a === '0') {
|
||||
return ['0', '0'];
|
||||
}
|
||||
|
||||
if ($a === $b) {
|
||||
return ['1', '0'];
|
||||
}
|
||||
|
||||
if ($b === '1') {
|
||||
return [$a, '0'];
|
||||
}
|
||||
|
||||
if ($b === '-1') {
|
||||
return [$this->neg($a), '0'];
|
||||
}
|
||||
|
||||
/** @var numeric-string $a */
|
||||
$na = $a * 1; // cast to number
|
||||
|
||||
if (is_int($na)) {
|
||||
/** @var numeric-string $b */
|
||||
$nb = $b * 1;
|
||||
|
||||
if (is_int($nb)) {
|
||||
// the only division that may overflow is PHP_INT_MIN / -1,
|
||||
// which cannot happen here as we've already handled a divisor of -1 above.
|
||||
$q = intdiv($na, $nb);
|
||||
$r = $na % $nb;
|
||||
|
||||
return [
|
||||
(string) $q,
|
||||
(string) $r,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
[$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
|
||||
|
||||
[$q, $r] = $this->doDiv($aDig, $bDig);
|
||||
|
||||
if ($aNeg !== $bNeg) {
|
||||
$q = $this->neg($q);
|
||||
}
|
||||
|
||||
if ($aNeg) {
|
||||
$r = $this->neg($r);
|
||||
}
|
||||
|
||||
return [$q, $r];
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function pow(string $a, int $e): string
|
||||
{
|
||||
if ($e === 0) {
|
||||
return '1';
|
||||
}
|
||||
|
||||
if ($e === 1) {
|
||||
return $a;
|
||||
}
|
||||
|
||||
$odd = $e % 2;
|
||||
$e -= $odd;
|
||||
|
||||
$aa = $this->mul($a, $a);
|
||||
|
||||
$result = $this->pow($aa, $e / 2);
|
||||
|
||||
if ($odd === 1) {
|
||||
$result = $this->mul($result, $a);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Algorithm from: https://www.geeksforgeeks.org/modular-exponentiation-power-in-modular-arithmetic/.
|
||||
*/
|
||||
#[Override]
|
||||
public function modPow(string $base, string $exp, string $mod): string
|
||||
{
|
||||
// normalize to Euclidean representative so modPow() stays consistent with mod()
|
||||
$base = $this->mod($base, $mod);
|
||||
|
||||
// special case: the algorithm below fails with power 0 mod 1 (returns 1 instead of 0)
|
||||
if ($exp === '0' && $mod === '1') {
|
||||
return '0';
|
||||
}
|
||||
|
||||
$x = $base;
|
||||
|
||||
$res = '1';
|
||||
|
||||
// numbers are positive, so we can use remainder instead of modulo
|
||||
$x = $this->divR($x, $mod);
|
||||
|
||||
while ($exp !== '0') {
|
||||
if (in_array($exp[-1], ['1', '3', '5', '7', '9'])) { // odd
|
||||
$res = $this->divR($this->mul($res, $x), $mod);
|
||||
}
|
||||
|
||||
$exp = $this->divQ($exp, '2');
|
||||
$x = $this->divR($this->mul($x, $x), $mod);
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapted from https://cp-algorithms.com/num_methods/roots_newton.html.
|
||||
*/
|
||||
#[Override]
|
||||
public function sqrt(string $n): string
|
||||
{
|
||||
if ($n === '0') {
|
||||
return '0';
|
||||
}
|
||||
|
||||
// initial approximation
|
||||
$x = str_repeat('9', intdiv(strlen($n), 2) ?: 1);
|
||||
|
||||
$decreased = false;
|
||||
|
||||
for (; ;) {
|
||||
$nx = $this->divQ($this->add($x, $this->divQ($n, $x)), '2');
|
||||
|
||||
if ($x === $nx || $this->cmp($nx, $x) > 0 && $decreased) {
|
||||
break;
|
||||
}
|
||||
|
||||
$decreased = $this->cmp($nx, $x) < 0;
|
||||
$x = $nx;
|
||||
}
|
||||
|
||||
return $x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the addition of two non-signed large integers.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
private function doAdd(string $a, string $b): string
|
||||
{
|
||||
[$a, $b, $length] = $this->pad($a, $b);
|
||||
|
||||
$carry = 0;
|
||||
$result = '';
|
||||
|
||||
for ($i = $length - $this->maxDigits; ; $i -= $this->maxDigits) {
|
||||
$blockLength = $this->maxDigits;
|
||||
|
||||
if ($i < 0) {
|
||||
$blockLength += $i;
|
||||
$i = 0;
|
||||
}
|
||||
|
||||
/** @var numeric-string $blockA */
|
||||
$blockA = substr($a, $i, $blockLength);
|
||||
|
||||
/** @var numeric-string $blockB */
|
||||
$blockB = substr($b, $i, $blockLength);
|
||||
|
||||
$sum = (string) ($blockA + $blockB + $carry);
|
||||
$sumLength = strlen($sum);
|
||||
|
||||
if ($sumLength > $blockLength) {
|
||||
$sum = substr($sum, 1);
|
||||
$carry = 1;
|
||||
} else {
|
||||
if ($sumLength < $blockLength) {
|
||||
$sum = str_repeat('0', $blockLength - $sumLength) . $sum;
|
||||
}
|
||||
$carry = 0;
|
||||
}
|
||||
|
||||
$result = $sum . $result;
|
||||
|
||||
if ($i === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($carry === 1) {
|
||||
$result = '1' . $result;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the subtraction of two non-signed large integers.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
private function doSub(string $a, string $b): string
|
||||
{
|
||||
if ($a === $b) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
// Ensure that we always subtract to a positive result: biggest minus smallest.
|
||||
$cmp = $this->doCmp($a, $b);
|
||||
|
||||
$invert = ($cmp === -1);
|
||||
|
||||
if ($invert) {
|
||||
$c = $a;
|
||||
$a = $b;
|
||||
$b = $c;
|
||||
}
|
||||
|
||||
[$a, $b, $length] = $this->pad($a, $b);
|
||||
|
||||
$carry = 0;
|
||||
$result = '';
|
||||
|
||||
$complement = 10 ** $this->maxDigits;
|
||||
|
||||
for ($i = $length - $this->maxDigits; ; $i -= $this->maxDigits) {
|
||||
$blockLength = $this->maxDigits;
|
||||
|
||||
if ($i < 0) {
|
||||
$blockLength += $i;
|
||||
$i = 0;
|
||||
}
|
||||
|
||||
/** @var numeric-string $blockA */
|
||||
$blockA = substr($a, $i, $blockLength);
|
||||
|
||||
/** @var numeric-string $blockB */
|
||||
$blockB = substr($b, $i, $blockLength);
|
||||
|
||||
$sum = $blockA - $blockB - $carry;
|
||||
|
||||
if ($sum < 0) {
|
||||
$sum += $complement;
|
||||
$carry = 1;
|
||||
} else {
|
||||
$carry = 0;
|
||||
}
|
||||
|
||||
$sum = (string) $sum;
|
||||
$sumLength = strlen($sum);
|
||||
|
||||
if ($sumLength < $blockLength) {
|
||||
$sum = str_repeat('0', $blockLength - $sumLength) . $sum;
|
||||
}
|
||||
|
||||
$result = $sum . $result;
|
||||
|
||||
if ($i === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Carry cannot be 1 when the loop ends, as a > b
|
||||
assert($carry === 0);
|
||||
|
||||
$result = ltrim($result, '0');
|
||||
|
||||
if ($invert) {
|
||||
$result = $this->neg($result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the multiplication of two non-signed large integers.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
private function doMul(string $a, string $b): string
|
||||
{
|
||||
$x = strlen($a);
|
||||
$y = strlen($b);
|
||||
|
||||
$maxDigits = intdiv($this->maxDigits, 2);
|
||||
$complement = 10 ** $maxDigits;
|
||||
|
||||
$result = '0';
|
||||
|
||||
for ($i = $x - $maxDigits; ; $i -= $maxDigits) {
|
||||
$blockALength = $maxDigits;
|
||||
|
||||
if ($i < 0) {
|
||||
$blockALength += $i;
|
||||
$i = 0;
|
||||
}
|
||||
|
||||
$blockA = (int) substr($a, $i, $blockALength);
|
||||
|
||||
$line = '';
|
||||
$carry = 0;
|
||||
|
||||
for ($j = $y - $maxDigits; ; $j -= $maxDigits) {
|
||||
$blockBLength = $maxDigits;
|
||||
|
||||
if ($j < 0) {
|
||||
$blockBLength += $j;
|
||||
$j = 0;
|
||||
}
|
||||
|
||||
$blockB = (int) substr($b, $j, $blockBLength);
|
||||
|
||||
$mul = $blockA * $blockB + $carry;
|
||||
$value = $mul % $complement;
|
||||
$carry = ($mul - $value) / $complement;
|
||||
|
||||
$value = (string) $value;
|
||||
$value = str_pad($value, $maxDigits, '0', STR_PAD_LEFT);
|
||||
|
||||
$line = $value . $line;
|
||||
|
||||
if ($j === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($carry !== 0) {
|
||||
$line = $carry . $line;
|
||||
}
|
||||
|
||||
$line = ltrim($line, '0');
|
||||
|
||||
if ($line !== '') {
|
||||
$line .= str_repeat('0', $x - $blockALength - $i);
|
||||
$result = $this->add($result, $line);
|
||||
}
|
||||
|
||||
if ($i === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the division of two non-signed large integers.
|
||||
*
|
||||
* @return string[] The quotient and remainder.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
private function doDiv(string $a, string $b): array
|
||||
{
|
||||
$cmp = $this->doCmp($a, $b);
|
||||
|
||||
if ($cmp === -1) {
|
||||
return ['0', $a];
|
||||
}
|
||||
|
||||
$x = strlen($a);
|
||||
$y = strlen($b);
|
||||
|
||||
// we now know that a >= b && x >= y
|
||||
|
||||
$q = '0'; // quotient
|
||||
$r = $a; // remainder
|
||||
$z = $y; // focus length, always $y or $y+1
|
||||
|
||||
/** @var numeric-string $b */
|
||||
$nb = $b * 1; // cast to number
|
||||
// performance optimization in cases where the remainder will never cause int overflow
|
||||
if (is_int(($nb - 1) * 10 + 9)) {
|
||||
$r = (int) substr($a, 0, $z - 1);
|
||||
|
||||
for ($i = $z - 1; $i < $x; $i++) {
|
||||
$n = $r * 10 + (int) $a[$i];
|
||||
/** @var int $nb */
|
||||
$q .= intdiv($n, $nb);
|
||||
$r = $n % $nb;
|
||||
}
|
||||
|
||||
return [ltrim($q, '0') ?: '0', (string) $r];
|
||||
}
|
||||
|
||||
for (; ;) {
|
||||
$focus = substr($a, 0, $z);
|
||||
|
||||
$cmp = $this->doCmp($focus, $b);
|
||||
|
||||
if ($cmp === -1) {
|
||||
if ($z === $x) { // remainder < dividend
|
||||
break;
|
||||
}
|
||||
|
||||
$z++;
|
||||
}
|
||||
|
||||
$zeros = str_repeat('0', $x - $z);
|
||||
|
||||
$q = $this->add($q, '1' . $zeros);
|
||||
$a = $this->sub($a, $b . $zeros);
|
||||
|
||||
$r = $a;
|
||||
|
||||
if ($r === '0') { // remainder == 0
|
||||
break;
|
||||
}
|
||||
|
||||
$x = strlen($a);
|
||||
|
||||
if ($x < $y) { // remainder < dividend
|
||||
break;
|
||||
}
|
||||
|
||||
$z = $y;
|
||||
}
|
||||
|
||||
return [$q, $r];
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two non-signed large numbers.
|
||||
*
|
||||
* @return -1|0|1
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
private function doCmp(string $a, string $b): int
|
||||
{
|
||||
$x = strlen($a);
|
||||
$y = strlen($b);
|
||||
|
||||
$cmp = $x <=> $y;
|
||||
|
||||
if ($cmp !== 0) {
|
||||
return $cmp;
|
||||
}
|
||||
|
||||
return strcmp($a, $b) <=> 0; // enforce -1|0|1
|
||||
}
|
||||
|
||||
/**
|
||||
* Pads the left of one of the given numbers with zeros if necessary to make both numbers the same length.
|
||||
*
|
||||
* The numbers must only consist of digits, without leading minus sign.
|
||||
*
|
||||
* @return array{string, string, int}
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
private function pad(string $a, string $b): array
|
||||
{
|
||||
$x = strlen($a);
|
||||
$y = strlen($b);
|
||||
|
||||
if ($x > $y) {
|
||||
$b = str_repeat('0', $x - $y) . $b;
|
||||
|
||||
return [$a, $b, $x];
|
||||
}
|
||||
|
||||
if ($x < $y) {
|
||||
$a = str_repeat('0', $y - $x) . $a;
|
||||
|
||||
return [$a, $b, $y];
|
||||
}
|
||||
|
||||
return [$a, $b, $x];
|
||||
}
|
||||
}
|
||||
74
vendor/brick/math/src/Internal/CalculatorRegistry.php
vendored
Normal file
74
vendor/brick/math/src/Internal/CalculatorRegistry.php
vendored
Normal file
@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Internal;
|
||||
|
||||
use function extension_loaded;
|
||||
|
||||
/**
|
||||
* Stores the current Calculator instance used by BigNumber classes.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class CalculatorRegistry
|
||||
{
|
||||
/**
|
||||
* The Calculator instance in use.
|
||||
*/
|
||||
private static ?Calculator $instance = null;
|
||||
|
||||
/**
|
||||
* Sets the Calculator instance to use.
|
||||
*
|
||||
* An instance is typically set only in unit tests: autodetect is usually the best option.
|
||||
*
|
||||
* @param Calculator|null $calculator The calculator instance, or null to revert to autodetect.
|
||||
*/
|
||||
final public static function set(?Calculator $calculator): void
|
||||
{
|
||||
self::$instance = $calculator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Calculator instance to use.
|
||||
*
|
||||
* If none has been explicitly set, the fastest available implementation will be returned.
|
||||
*
|
||||
* Note: even though this method is not technically pure, it is considered pure when used in a normal context, when
|
||||
* only relying on autodetect.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
final public static function get(): Calculator
|
||||
{
|
||||
/** @phpstan-ignore impure.staticPropertyAccess */
|
||||
if (self::$instance === null) {
|
||||
/** @phpstan-ignore impure.propertyAssign */
|
||||
self::$instance = self::detect();
|
||||
}
|
||||
|
||||
/** @phpstan-ignore impure.staticPropertyAccess */
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the fastest available Calculator implementation.
|
||||
*
|
||||
* @pure
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
private static function detect(): Calculator
|
||||
{
|
||||
if (extension_loaded('gmp')) {
|
||||
return new Calculator\GmpCalculator();
|
||||
}
|
||||
|
||||
if (extension_loaded('bcmath')) {
|
||||
return new Calculator\BcMathCalculator();
|
||||
}
|
||||
|
||||
return new Calculator\NativeCalculator();
|
||||
}
|
||||
}
|
||||
177
vendor/brick/math/src/Internal/DecimalHelper.php
vendored
Normal file
177
vendor/brick/math/src/Internal/DecimalHelper.php
vendored
Normal file
@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Internal;
|
||||
|
||||
use Brick\Math\RoundingMode;
|
||||
|
||||
use function ltrim;
|
||||
use function rtrim;
|
||||
use function str_pad;
|
||||
use function str_repeat;
|
||||
use function strlen;
|
||||
use function substr;
|
||||
|
||||
use const STR_PAD_LEFT;
|
||||
|
||||
/**
|
||||
* Shared helper for decimal operations.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class DecimalHelper
|
||||
{
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the scale needed to represent the exact decimal result of a reduced fraction.
|
||||
*
|
||||
* Returns null if the denominator has prime factors other than 2 or 5.
|
||||
*
|
||||
* @param string $denominator The denominator of the reduced fraction. Must be strictly positive.
|
||||
*
|
||||
* @return non-negative-int|null
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function computeScaleFromReducedFractionDenominator(string $denominator): ?int
|
||||
{
|
||||
$calculator = CalculatorRegistry::get();
|
||||
|
||||
$d = rtrim($denominator, '0');
|
||||
|
||||
/** @var non-negative-int $scale rtrim can only shorten a string */
|
||||
$scale = strlen($denominator) - strlen($d);
|
||||
|
||||
foreach ([5, 2] as $prime) {
|
||||
for (; ;) {
|
||||
$lastDigit = (int) $d[-1];
|
||||
|
||||
if ($lastDigit % $prime !== 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
$d = $calculator->divQ($d, (string) $prime);
|
||||
$scale++;
|
||||
}
|
||||
}
|
||||
|
||||
return $d === '1' ? $scale : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scales an unscaled decimal value to the requested scale.
|
||||
*
|
||||
* Returns null when rounding is necessary and the rounding mode is Unnecessary.
|
||||
*
|
||||
* @param string $value The unscaled value.
|
||||
* @param int $currentScale The current scale.
|
||||
* @param int $targetScale The target scale.
|
||||
* @param RoundingMode $roundingMode The rounding mode.
|
||||
*
|
||||
* @return string|null The unscaled value at the target scale, or null if RoundingMode::Unnecessary is used and rounding is necessary.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function scale(string $value, int $currentScale, int $targetScale, RoundingMode $roundingMode): ?string
|
||||
{
|
||||
$scaled = self::tryScaleExactly($value, $currentScale, $targetScale);
|
||||
|
||||
if ($scaled !== null) {
|
||||
return $scaled;
|
||||
}
|
||||
|
||||
if ($roundingMode === RoundingMode::Unnecessary) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$divisor = '1' . str_repeat('0', $currentScale - $targetScale);
|
||||
|
||||
return CalculatorRegistry::get()->divRound($value, $divisor, $roundingMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds leading zeros if necessary to represent the full decimal number.
|
||||
*
|
||||
* @param string $value The unscaled value.
|
||||
* @param int $scale The current scale.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function padUnscaledValue(string $value, int $scale): string
|
||||
{
|
||||
$targetLength = $scale + 1;
|
||||
$negative = ($value[0] === '-');
|
||||
$length = strlen($value);
|
||||
|
||||
if ($negative) {
|
||||
$length--;
|
||||
}
|
||||
|
||||
if ($length >= $targetLength) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if ($negative) {
|
||||
$value = substr($value, 1);
|
||||
}
|
||||
|
||||
$value = str_pad($value, $targetLength, '0', STR_PAD_LEFT);
|
||||
|
||||
if ($negative) {
|
||||
$value = '-' . $value;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to scale exactly without rounding, returning null when rounding would be required.
|
||||
*
|
||||
* @param string $value The unscaled value.
|
||||
* @param int $currentScale The current scale.
|
||||
* @param int $targetScale The target scale.
|
||||
*
|
||||
* @return string|null The unscaled value at the target scale, or null if rounding would be required.
|
||||
*
|
||||
* @pure
|
||||
*/
|
||||
public static function tryScaleExactly(string $value, int $currentScale, int $targetScale): ?string
|
||||
{
|
||||
if ($value === '0' || $targetScale === $currentScale) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if ($targetScale > $currentScale) {
|
||||
return $value . str_repeat('0', $targetScale - $currentScale);
|
||||
}
|
||||
|
||||
$negative = ($value[0] === '-');
|
||||
if ($negative) {
|
||||
$value = substr($value, 1);
|
||||
}
|
||||
|
||||
$value = self::padUnscaledValue($value, $currentScale);
|
||||
$discardedDigits = $currentScale - $targetScale;
|
||||
|
||||
if (substr($value, -$discardedDigits) !== str_repeat('0', $discardedDigits)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$value = substr($value, 0, -$discardedDigits);
|
||||
$value = ltrim($value, '0');
|
||||
|
||||
if ($value === '') {
|
||||
return '0';
|
||||
}
|
||||
|
||||
if ($negative) {
|
||||
$value = '-' . $value;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
81
vendor/brick/math/src/Internal/Safe.php
vendored
Normal file
81
vendor/brick/math/src/Internal/Safe.php
vendored
Normal file
@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math\Internal;
|
||||
|
||||
use Brick\Math\Exception\IntegerOverflowException;
|
||||
|
||||
use function is_int;
|
||||
use function sprintf;
|
||||
|
||||
use const PHP_INT_MIN;
|
||||
|
||||
/**
|
||||
* Helpers for arithmetic operations that throw on native integer overflow.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class Safe
|
||||
{
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @pure
|
||||
*/
|
||||
public static function add(int $a, int $b): int
|
||||
{
|
||||
$result = $a + $b;
|
||||
|
||||
if (is_int($result)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// @phpstan-ignore deadCode.unreachable
|
||||
throw IntegerOverflowException::nativeIntegerOverflow(sprintf('%d + %d', $a, $b));
|
||||
}
|
||||
|
||||
/**
|
||||
* @pure
|
||||
*/
|
||||
public static function sub(int $a, int $b): int
|
||||
{
|
||||
$result = $a - $b;
|
||||
|
||||
if (is_int($result)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// @phpstan-ignore deadCode.unreachable
|
||||
throw IntegerOverflowException::nativeIntegerOverflow(sprintf('%d - %d', $a, $b));
|
||||
}
|
||||
|
||||
/**
|
||||
* @pure
|
||||
*/
|
||||
public static function mul(int $a, int $b): int
|
||||
{
|
||||
$result = $a * $b;
|
||||
|
||||
if (is_int($result)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// @phpstan-ignore deadCode.unreachable
|
||||
throw IntegerOverflowException::nativeIntegerOverflow(sprintf('%d * %d', $a, $b));
|
||||
}
|
||||
|
||||
/**
|
||||
* @pure
|
||||
*/
|
||||
public static function neg(int $value): int
|
||||
{
|
||||
if ($value === PHP_INT_MIN) {
|
||||
throw IntegerOverflowException::nativeIntegerOverflow(sprintf('-(%d)', $value));
|
||||
}
|
||||
|
||||
return -$value;
|
||||
}
|
||||
}
|
||||
93
vendor/brick/math/src/RoundingMode.php
vendored
Normal file
93
vendor/brick/math/src/RoundingMode.php
vendored
Normal file
@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Brick\Math;
|
||||
|
||||
/**
|
||||
* Specifies rounding behavior by defining how discarded digits affect the returned result when an exact value cannot
|
||||
* be represented at the requested scale.
|
||||
*/
|
||||
enum RoundingMode
|
||||
{
|
||||
/**
|
||||
* Asserts that the requested operation has an exact result, hence no rounding is necessary.
|
||||
*
|
||||
* If this rounding mode is specified on an operation that yields a result that
|
||||
* cannot be represented at the requested scale, a RoundingNecessaryException is thrown.
|
||||
*/
|
||||
case Unnecessary;
|
||||
|
||||
/**
|
||||
* Rounds away from zero.
|
||||
*
|
||||
* Always increments the digit prior to a nonzero discarded fraction.
|
||||
* Note that this rounding mode never decreases the magnitude of the calculated value.
|
||||
*/
|
||||
case Up;
|
||||
|
||||
/**
|
||||
* Rounds towards zero.
|
||||
*
|
||||
* Never increments the digit prior to a discarded fraction (i.e., truncates).
|
||||
* Note that this rounding mode never increases the magnitude of the calculated value.
|
||||
*/
|
||||
case Down;
|
||||
|
||||
/**
|
||||
* Rounds towards positive infinity.
|
||||
*
|
||||
* If the result is positive, behaves as for Up; if negative, behaves as for Down.
|
||||
* Note that this rounding mode never decreases the calculated value.
|
||||
*/
|
||||
case Ceiling;
|
||||
|
||||
/**
|
||||
* Rounds towards negative infinity.
|
||||
*
|
||||
* If the result is positive, behaves as for Down; if negative, behaves as for Up.
|
||||
* Note that this rounding mode never increases the calculated value.
|
||||
*/
|
||||
case Floor;
|
||||
|
||||
/**
|
||||
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round up.
|
||||
*
|
||||
* Behaves as for Up if the discarded fraction is >= 0.5; otherwise, behaves as for Down.
|
||||
* Note that this is the rounding mode commonly taught at school.
|
||||
*/
|
||||
case HalfUp;
|
||||
|
||||
/**
|
||||
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round down.
|
||||
*
|
||||
* Behaves as for Up if the discarded fraction is > 0.5; otherwise, behaves as for Down.
|
||||
*/
|
||||
case HalfDown;
|
||||
|
||||
/**
|
||||
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round towards positive infinity.
|
||||
*
|
||||
* If the result is positive, behaves as for HalfUp; if negative, behaves as for HalfDown.
|
||||
*/
|
||||
case HalfCeiling;
|
||||
|
||||
/**
|
||||
* Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round towards negative infinity.
|
||||
*
|
||||
* If the result is positive, behaves as for HalfDown; if negative, behaves as for HalfUp.
|
||||
*/
|
||||
case HalfFloor;
|
||||
|
||||
/**
|
||||
* Rounds towards the "nearest neighbor" unless both neighbors are equidistant, in which case rounds towards the even neighbor.
|
||||
*
|
||||
* Behaves as for HalfUp if the digit to the left of the discarded fraction is odd;
|
||||
* behaves as for HalfDown if it's even.
|
||||
*
|
||||
* Note that this is the rounding mode that statistically minimizes
|
||||
* cumulative error when applied repeatedly over a sequence of calculations.
|
||||
* It is sometimes known as "Banker's rounding", and is chiefly used in the USA.
|
||||
*/
|
||||
case HalfEven;
|
||||
}
|
||||
21
vendor/carbonphp/carbon-doctrine-types/LICENSE
vendored
Normal file
21
vendor/carbonphp/carbon-doctrine-types/LICENSE
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Carbon
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
14
vendor/carbonphp/carbon-doctrine-types/README.md
vendored
Normal file
14
vendor/carbonphp/carbon-doctrine-types/README.md
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
# carbonphp/carbon-doctrine-types
|
||||
|
||||
Types to use Carbon in Doctrine
|
||||
|
||||
## Documentation
|
||||
|
||||
[Check how to use in the official Carbon documentation](https://carbon.nesbot.com/symfony/)
|
||||
|
||||
This package is an externalization of [src/Carbon/Doctrine](https://github.com/briannesbitt/Carbon/tree/2.71.0/src/Carbon/Doctrine)
|
||||
from `nestbot/carbon` package.
|
||||
|
||||
Externalization allows to better deal with different versions of dbal. With
|
||||
version 4.0 of dbal, it no longer sustainable to be compatible with all version
|
||||
using a single code.
|
||||
36
vendor/carbonphp/carbon-doctrine-types/composer.json
vendored
Normal file
36
vendor/carbonphp/carbon-doctrine-types/composer.json
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "carbonphp/carbon-doctrine-types",
|
||||
"description": "Types to use Carbon in Doctrine",
|
||||
"type": "library",
|
||||
"keywords": [
|
||||
"date",
|
||||
"time",
|
||||
"DateTime",
|
||||
"Carbon",
|
||||
"Doctrine"
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/dbal": "^4.0.0",
|
||||
"nesbot/carbon": "^2.71.0 || ^3.0.0",
|
||||
"phpunit/phpunit": "^10.3"
|
||||
},
|
||||
"conflict": {
|
||||
"doctrine/dbal": "<4.0.0 || >=5.0.0"
|
||||
},
|
||||
"license": "MIT",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Carbon\\Doctrine\\": "src/Carbon/Doctrine/"
|
||||
}
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "KyleKatarn",
|
||||
"email": "kylekatarnls@gmail.com"
|
||||
}
|
||||
],
|
||||
"minimum-stability": "dev"
|
||||
}
|
||||
16
vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/CarbonDoctrineType.php
vendored
Normal file
16
vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/CarbonDoctrineType.php
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Carbon\Doctrine;
|
||||
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
|
||||
interface CarbonDoctrineType
|
||||
{
|
||||
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform);
|
||||
|
||||
public function convertToPHPValue(mixed $value, AbstractPlatform $platform);
|
||||
|
||||
public function convertToDatabaseValue($value, AbstractPlatform $platform);
|
||||
}
|
||||
9
vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/CarbonImmutableType.php
vendored
Normal file
9
vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/CarbonImmutableType.php
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Carbon\Doctrine;
|
||||
|
||||
class CarbonImmutableType extends DateTimeImmutableType implements CarbonDoctrineType
|
||||
{
|
||||
}
|
||||
9
vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/CarbonType.php
vendored
Normal file
9
vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/CarbonType.php
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Carbon\Doctrine;
|
||||
|
||||
class CarbonType extends DateTimeType implements CarbonDoctrineType
|
||||
{
|
||||
}
|
||||
131
vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/CarbonTypeConverter.php
vendored
Normal file
131
vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/CarbonTypeConverter.php
vendored
Normal file
@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Carbon\Doctrine;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Carbon\CarbonInterface;
|
||||
use DateTimeInterface;
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\DBAL\Platforms\DB2Platform;
|
||||
use Doctrine\DBAL\Platforms\OraclePlatform;
|
||||
use Doctrine\DBAL\Platforms\SQLitePlatform;
|
||||
use Doctrine\DBAL\Platforms\SQLServerPlatform;
|
||||
use Doctrine\DBAL\Types\Exception\InvalidType;
|
||||
use Doctrine\DBAL\Types\Exception\ValueNotConvertible;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* @template T of CarbonInterface
|
||||
*/
|
||||
trait CarbonTypeConverter
|
||||
{
|
||||
/**
|
||||
* This property differentiates types installed by carbonphp/carbon-doctrine-types
|
||||
* from the ones embedded previously in nesbot/carbon source directly.
|
||||
*
|
||||
* @readonly
|
||||
*/
|
||||
public bool $external = true;
|
||||
|
||||
/**
|
||||
* @return class-string<T>
|
||||
*/
|
||||
protected function getCarbonClassName(): string
|
||||
{
|
||||
return Carbon::class;
|
||||
}
|
||||
|
||||
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): string
|
||||
{
|
||||
$precision = min(
|
||||
$fieldDeclaration['precision'] ?? DateTimeDefaultPrecision::get(),
|
||||
$this->getMaximumPrecision($platform),
|
||||
);
|
||||
|
||||
$type = parent::getSQLDeclaration($fieldDeclaration, $platform);
|
||||
|
||||
if (!$precision) {
|
||||
return $type;
|
||||
}
|
||||
|
||||
if (str_contains($type, '(')) {
|
||||
return preg_replace('/\(\d+\)/', "($precision)", $type);
|
||||
}
|
||||
|
||||
[$before, $after] = explode(' ', "$type ");
|
||||
|
||||
return trim("$before($precision) $after");
|
||||
}
|
||||
|
||||
/**
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string
|
||||
{
|
||||
if ($value === null) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if ($value instanceof DateTimeInterface) {
|
||||
return $value->format('Y-m-d H:i:s.u');
|
||||
}
|
||||
|
||||
throw InvalidType::new(
|
||||
$value,
|
||||
static::class,
|
||||
['null', 'DateTime', 'Carbon']
|
||||
);
|
||||
}
|
||||
|
||||
private function doConvertToPHPValue(mixed $value)
|
||||
{
|
||||
$class = $this->getCarbonClassName();
|
||||
|
||||
if ($value === null || is_a($value, $class)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if ($value instanceof DateTimeInterface) {
|
||||
return $class::instance($value);
|
||||
}
|
||||
|
||||
$date = null;
|
||||
$error = null;
|
||||
|
||||
try {
|
||||
$date = $class::parse($value);
|
||||
} catch (Exception $exception) {
|
||||
$error = $exception;
|
||||
}
|
||||
|
||||
if (!$date) {
|
||||
throw ValueNotConvertible::new(
|
||||
$value,
|
||||
static::class,
|
||||
'Y-m-d H:i:s.u or any format supported by '.$class.'::parse()',
|
||||
$error
|
||||
);
|
||||
}
|
||||
|
||||
return $date;
|
||||
}
|
||||
|
||||
private function getMaximumPrecision(AbstractPlatform $platform): int
|
||||
{
|
||||
if ($platform instanceof DB2Platform) {
|
||||
return 12;
|
||||
}
|
||||
|
||||
if ($platform instanceof OraclePlatform) {
|
||||
return 9;
|
||||
}
|
||||
|
||||
if ($platform instanceof SQLServerPlatform || $platform instanceof SQLitePlatform) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
return 6;
|
||||
}
|
||||
}
|
||||
30
vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/DateTimeDefaultPrecision.php
vendored
Normal file
30
vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/DateTimeDefaultPrecision.php
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Carbon\Doctrine;
|
||||
|
||||
class DateTimeDefaultPrecision
|
||||
{
|
||||
private static $precision = 6;
|
||||
|
||||
/**
|
||||
* Change the default Doctrine datetime and datetime_immutable precision.
|
||||
*
|
||||
* @param int $precision
|
||||
*/
|
||||
public static function set(int $precision): void
|
||||
{
|
||||
self::$precision = $precision;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default Doctrine datetime and datetime_immutable precision.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function get(): int
|
||||
{
|
||||
return self::$precision;
|
||||
}
|
||||
}
|
||||
32
vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/DateTimeImmutableType.php
vendored
Normal file
32
vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/DateTimeImmutableType.php
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Carbon\Doctrine;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\DBAL\Types\VarDateTimeImmutableType;
|
||||
|
||||
class DateTimeImmutableType extends VarDateTimeImmutableType implements CarbonDoctrineType
|
||||
{
|
||||
/** @use CarbonTypeConverter<CarbonImmutable> */
|
||||
use CarbonTypeConverter;
|
||||
|
||||
/**
|
||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||
*/
|
||||
public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?CarbonImmutable
|
||||
{
|
||||
return $this->doConvertToPHPValue($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return class-string<CarbonImmutable>
|
||||
*/
|
||||
protected function getCarbonClassName(): string
|
||||
{
|
||||
return CarbonImmutable::class;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user