# 機能設計書 148-replace_with

## 概要

本ドキュメントは、Hack Standard Library (HSL) の `Regex\replace_with` 関数の機能設計を定義する。正規表現パターンにマッチする部分を、コールバック関数の結果で置換する関数である。

### 本機能の処理概要

**業務上の目的・背景**：単純な文字列置換では対応できない、動的な置換処理が必要な場面がある。本機能は、各マッチに対してコールバック関数を呼び出し、その戻り値で置換を行う。キャプチャグループの値に基づいた複雑な変換処理が可能になる。

**機能の利用シーン**：テキストのフォーマット変換（マッチ内容に基づく動的変換）、URLやメンションの展開、条件付きテキスト処理など、マッチ内容に応じた動的置換が必要な場面で利用される。

**主要な処理内容**：
1. オフセット位置から検索を開始
2. マッチが見つかるたびにコールバック関数を呼び出し
3. コールバックの戻り値で置換
4. 全てのマッチを処理後、結果文字列を返却

**関連システム・外部連携**：PCREライブラリに依存する正規表現エンジンを使用。

**権限による制御**：権限による制御は存在しない。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| - | 該当なし | - | 本プロジェクトはUIを持たないライブラリ |

## 機能種別

データ処理 / 文字列操作

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| $haystack | string | Yes | 置換対象の文字列 | なし |
| $pattern | Pattern<T> | Yes | PCRE正規表現パターン | PCREパターンとして有効であること |
| $replace_func | (function(T)[_]: string) | Yes | マッチを受け取り置換文字列を返すコールバック | なし |
| $offset | int | No | 置換開始位置（デフォルト: 0） | 文字列長の正負範囲内 |

### 入力データソース

関数呼び出し時の引数

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| 戻り値 | string | 置換後の文字列 |

### 出力先

関数の戻り値として返却

## 処理フロー

### 処理シーケンス

```
1. 文字列長を取得
2. 結果文字列を空で初期化
3. マッチ終了位置を0で初期化
4. ループ開始
   └─ マッチを検索
5. マッチなしの場合ループ終了
6. 前回マッチから今回マッチまでの部分をコピー
7. コールバック関数を呼び出し
8. コールバックの結果を結果文字列に追加
9. マッチ終了位置を更新
10. 空マッチの場合オフセットを1進める
11. ループ継続
12. 最後のマッチ以降の部分を追加
13. 結果文字列を返却
```

### フローチャート

```mermaid
flowchart TD
    A[開始] --> B[初期化]
    B --> C[マッチを検索]
    C --> D{マッチあり?}
    D -->|No| E[残りの文字列を追加]
    D -->|Yes| F[前回から今回までをコピー]
    F --> G[コールバック呼び出し]
    G --> H[結果を追加]
    H --> I[位置を更新]
    I --> J{空マッチ?}
    J -->|Yes| K[オフセット++]
    J -->|No| L[オフセット = マッチ終了位置]
    K --> M{オフセット > 文字列長?}
    M -->|Yes| E
    M -->|No| C
    L --> C
    E --> N[結果を返却]
    N --> O[終了]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-148-1 | コールバック置換 | 各マッチに対してコールバックを呼び出し、その結果で置換 | 常時 |
| BR-148-2 | マッチオブジェクト渡し | コールバックにはMatch shape全体が渡される | 常時 |
| BR-148-3 | 空マッチ処理 | 空マッチの場合、オフセットを1進めて次の検索 | マッチ長が0の場合 |
| BR-148-4 | 部分保持 | マッチしない部分はそのまま結果に含まれる | 常時 |

### 計算ロジック

- 前回から今回までの部分: `Str\slice($haystack, $match_end, $offset - $match_end)`
- コールバック呼び出し: `$replace_func($match)`
- マッチ長: `Str\length(Shapes::at($match, 0))`

## データベース操作仕様

該当なし（データベース操作を行わない純粋関数）

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | InvariantException | オフセットが範囲外 | オフセット値を修正 |
| PREG_* | Regex\Exception | PCREエラー | first_matchと同様 |
| - | コールバック例外 | コールバック内での例外 | コールバック実装を修正 |

### リトライ仕様

該当なし

## トランザクション仕様

該当なし（データベース操作を行わない）

## パフォーマンス要件

- 時間計算量: O(n * m * f) - fはコールバックの計算量
- 空間計算量: O(n) - 置換結果を格納

## セキュリティ考慮事項

- ReDoS（正規表現DoS）攻撃に注意
- コールバック関数の安全性に注意
- `[ctx $replace_func]`: コールバックのコンテキストに依存

## 備考

- 単純な文字列置換には`Regex\replace()`を使用
- コールバックはキャプチャグループにアクセス可能

---

## コードリーディングガイド

本機能を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: データ構造を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | regex.php | `src/regex/regex.php` | Pattern<T>型、コールバック関数の型を確認 |

**読解のコツ**:
- `(function(T)[_]: string)` はマッチを受け取り文字列を返すコールバック
- `[_]` は任意のコンテキストを許容することを示す
- `[ctx $replace_func]` は関数のコンテキストがコールバックに依存することを示す

#### Step 2: エントリーポイントを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | regex.php | `src/regex/regex.php` | replace_with関数の定義（140-173行目） |

**主要処理フロー**:
1. **146行目**: `Str\length($haystack)` で文字列長を取得
2. **147行目**: `Str\slice($haystack, 0, 0)` で結果を空文字列で初期化
3. **148行目**: `$match_end = 0` でマッチ終了位置を初期化
4. **149行目**: `while (true)` でループ開始
5. **150行目**: `_Private\regex_match` でマッチを検索
6. **151-153行目**: マッチなしの場合break
7. **155行目**: 前回マッチから今回マッチまでの部分をコピー
8. **156行目**: コールバック呼び出し `$replace_func($match)`
9. **157-158行目**: マッチ長とマッチ終了位置を更新
10. **159-168行目**: 空マッチ処理
11. **171行目**: 残りの文字列を追加
12. **172行目**: 結果を返却

#### Step 3: 空マッチ処理を理解する

**重要なロジック（159-168行目）**:
```hack
if ($match_length === 0) {
  // To get the next match (and avoid looping forever), need to skip forward
  // before searching again
  // Note that `$offset` is for searching and `$match_end` is for copying
  $offset++;
  if ($offset > $haystack_length) {
    break;
  }
} else {
  $offset = $match_end;
}
```

`$offset`は検索位置、`$match_end`はコピー位置という役割分担に注意。

### プログラム呼び出し階層図

```
Regex\replace_with($haystack, $pattern, $replace_func, $offset)
    |
    +-- Str\length($haystack)
    |
    +-- while (true)
            |
            +-- _Private\regex_match($haystack, $pattern, inout $offset)
            |       └─ PCREマッチング
            |
            +-- Str\slice($haystack, $match_end, $offset - $match_end)
            |       └─ マッチ間の部分をコピー
            |
            +-- $replace_func($match)
            |       └─ コールバック呼び出し
            |
            +-- Str\length(Shapes::at($match, 0))
                    └─ マッチ長計算
```

### データフロー図

```
[入力]                    [処理]                      [出力]

$haystack ─────┐
               |
$pattern ──────┼──> whileループ ───> 繰り返しマッチング
               |         │
$replace_func ─┤         v
               |    マッチごとにコールバック呼び出し
$offset ───────┘         │
                         v
                    string ────────> 戻り値
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| regex.php | `src/regex/regex.php` | ソース | replace_with関数の公開インターフェース |
| private.php | `src/regex/private.php` | ソース | regex_match内部実装 |
| RegexTest.php | `tests/regex/RegexTest.php` | テスト | replace_with関数のテストケース |
