2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rust製の日付解析ライブラリ「Universal Date Parser」

Last updated at Posted at 2025-09-22

はじめに

Universal Date Parserは、あらゆる形式の日付を自動的に解析し、標準化された出力に変換する高性能なRustライブラリです。パフォーマンス、信頼性、使いやすさを重視して設計されており、自動フォーマット検出、タイムゾーン対応、多言語バインディングを提供します。

ライブラリの特徴

主要な機能

  • 自動フォーマット検出: 複数の日付フォーマットを自動で認識
  • 高性能: 1回の解析あたり300-600ナノ秒の超高速処理
  • タイムゾーン対応: 柔軟なタイムゾーン処理オプション
  • 多言語対応: C FFI、WebAssembly、Python対応
  • ゼロコピー: 可能な限りメモリコピーを避けた効率的な実装
  • 曖昧性解決: 地域固有のフォーマット(MM/DD vs DD/MM)の賢い判定

サポートする日付フォーマット

ISO 8601標準

// 完全なISO 8601サポート
"2023-12-25T10:30:00Z"          // UTC日時
"2023-12-25T10:30:00+09:00"     // タイムゾーン付き
"2023-12-25"                    // 日付のみ
"20231225T103000Z"              // コンパクト形式

地域別フォーマット

// アメリカ式(月/日/年)
"12/25/2023"    // MM/DD/YYYY

// ヨーロッパ式(日/月/年)
"25/12/2023"    // DD/MM/YYYY

// 日本式
"2023年12月25日"
"2023/12/25"

Unixタイムスタンプ

// 秒精度
"1703520645"        // エポックからの秒数

// ミリ秒精度
"1703520645000"     // エポックからのミリ秒数

アーキテクチャ概要

コアコンポーネント

ParsedDate構造体

解析成功後の日付データを表現する基本構造体:

#[derive(Debug, Clone, PartialEq)]
pub struct ParsedDate {
    pub year: i32,
    pub month: u32,
    pub day: u32,
    pub hour: Option<u32>,
    pub minute: Option<u32>,
    pub second: Option<u32>,
    pub timezone_offset: Option<i32>,
    pub detected_format: String,
}

UniversalDateParser

設定可能なオプションを持つメイン解析エンジン:

pub struct UniversalDateParser {
    timezone_mode: TimezoneMode,
    ambiguity_resolver: AmbiguityResolver,
}

フォーマット検出エンジン

自動パターンマッチングによる賢いフォーマット認識:

pub fn detect_format(&self, input: &str) -> Option<&'static str> {
    // ISO 8601フォーマット(最優先)
    if ISO_DATETIME_REGEX.is_match(input) {
        return Some("ISO 8601 DateTime");
    }
    
    // Unixタイムスタンプ
    if UNIX_TIMESTAMP_REGEX.is_match(input) {
        return if input.len() > 10 {
            Some("Unix Timestamp (ms)")
        } else {
            Some("Unix Timestamp")
        };
    }
    
    // 地域フォーマットと曖昧性解決
    // ... 高度なパターンマッチング
}

設計原則

パフォーマンス最優先

  • 可能な限りゼロコピー解析
  • lazy_staticを使用した効率的な正規表現コンパイル
  • 早期リターンによる効率的なパターンマッチング
  • ベンチマーク結果:1回の解析あたり300-600ns

インテリジェント検出

解析エンジンは以下の優先順位でフォーマットを検出:

  1. ISO 8601標準(最高優先度)
  2. Unixタイムスタンプ
  3. 明確な地域フォーマット
  4. 曖昧なフォーマットの文脈解決

タイムゾーン対応

柔軟なタイムゾーン処理オプション:

  • AssumeUtc: すべての日付をUTCとして処理(最高速)
  • AssumeLocal: システムタイムゾーンを使用
  • PreserveOffset: 元のタイムゾーン情報を保持
  • ConvertToUtc: すべての日付をUTCに変換

インストールと基本的な使用方法

Cargoプロジェクトへの追加

[dependencies]
universal-date-parser = "0.1.0"

基本的な使用例

use universal_date_parser::{UniversalDateParser, TimezoneMode};

fn main() {
    let parser = UniversalDateParser::new(TimezoneMode::AssumeUtc);
    
    // 様々なフォーマットの解析
    let inputs = vec![
        "2023-12-25T10:30:00Z",
        "12/25/2023",
        "25/12/2023",
        "1703520645",
        "2023年12月25日",
    ];
    
    for input in inputs {
        match parser.parse(input) {
            Ok(parsed) => {
                println!("入力: {} -> 解析結果: {:?}", input, parsed);
                println!("検出されたフォーマット: {}", parsed.detected_format);
            },
            Err(e) => println!("解析エラー: {}", e),
        }
    }
}

高度な設定例

use universal_date_parser::{
    UniversalDateParser, 
    TimezoneMode, 
    AmbiguityResolver
};

fn main() {
    // カスタム設定でパーサーを作成
    let parser = UniversalDateParser::builder()
        .timezone_mode(TimezoneMode::PreserveOffset)
        .ambiguity_resolver(AmbiguityResolver::PreferEuropean)
        .build();
    
    // 曖昧な日付の解析
    let ambiguous_date = "01/02/2023";
    match parser.parse(ambiguous_date) {
        Ok(parsed) => {
            // PreferEuropeanの設定により、01/02/2023は2月1日として解釈
            println!("解析結果: {}-{:02}-{:02}", 
                parsed.year, parsed.month, parsed.day);
        },
        Err(e) => println!("エラー: {}", e),
    }
}

パフォーマンス特性

ベンチマーク結果

Criterionを使用した包括的なベンチマークに基づく結果:

フォーマットタイプ 平均時間 スループット
ISO 8601 DateTime 388ns 2.6M ops/sec
ISO 8601 Date 344ns 2.9M ops/sec
アメリカ式フォーマット 392ns 2.6M ops/sec
ヨーロッパ式フォーマット 618ns 1.6M ops/sec
Unixタイムスタンプ 483ns 2.1M ops/sec
Unixタイムスタンプ(ms) 598ns 1.7M ops/sec

メモリ使用量

  • 最小限のヒープアロケーション
  • 静的正規表現パターンの使用
  • スタックベースの解析でメモリ効率を最大化

最適化手法

ゼロコピー解析

// 文字列のコピーを避けた効率的な解析
pub fn parse_iso_date(&self, input: &str) -> Result<ParsedDate, ParseError> {
    // 直接文字列スライスを操作してパフォーマンス向上
    let year: i32 = input[0..4].parse()?;
    let month: u32 = input[5..7].parse()?;
    let day: u32 = input[8..10].parse()?;
    // ...
}

遅延正規表現コンパイル

lazy_static! {
    static ref ISO_DATETIME_REGEX: Regex = 
        Regex::new(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$")
        .unwrap();
}

多言語バインディング

C FFI

C言語からの使用例:

#include "universal_date_parser.h"

int main() {
    UniversalDateParser* parser = universal_parser_new();
    
    ParsedDate result;
    int status = universal_parser_parse(parser, "2023-12-25", &result);
    
    if (status == 0) {
        printf("解析成功: %d-%d-%d\n", result.year, result.month, result.day);
    }
    
    universal_parser_free(parser);
    return 0;
}

WebAssembly

JavaScript/TypeScriptからの使用:

import { UniversalDateParser } from './pkg/universal_date_parser.js';

const parser = new UniversalDateParser();
const result = parser.parse("2023-12-25T10:30:00Z");

console.log(`解析結果: ${result.year}-${result.month}-${result.day}`);
console.log(`検出フォーマット: ${result.detected_format}`);

Python バインディング

PyO3を使用したPython対応:

import universal_date_parser

parser = universal_date_parser.UniversalDateParser()
result = parser.parse("2023-12-25T10:30:00Z")

print(f"解析結果: {result.year}-{result.month}-{result.day}")
print(f"検出フォーマット: {result.detected_format}")

実用的な使用例

ログ解析システム

use universal_date_parser::{UniversalDateParser, TimezoneMode};
use std::collections::HashMap;

struct LogAnalyzer {
    parser: UniversalDateParser,
    stats: HashMap<String, u32>,
}

impl LogAnalyzer {
    fn new() -> Self {
        Self {
            parser: UniversalDateParser::new(TimezoneMode::AssumeUtc),
            stats: HashMap::new(),
        }
    }
    
    fn analyze_log_entry(&mut self, log_line: &str) {
        // ログエントリから日付部分を抽出
        if let Some(date_part) = self.extract_date_from_log(log_line) {
            match self.parser.parse(&date_part) {
                Ok(parsed) => {
                    let format = parsed.detected_format.clone();
                    *self.stats.entry(format).or_insert(0) += 1;
                },
                Err(_) => {
                    *self.stats.entry("unknown".to_string()).or_insert(0) += 1;
                }
            }
        }
    }
    
    fn extract_date_from_log(&self, log_line: &str) -> Option<String> {
        // ログ形式に応じた日付抽出ロジック
        // 実際の実装では正規表現などを使用
        None
    }
}

データベース移行ツール

use universal_date_parser::{UniversalDateParser, TimezoneMode};

struct DataMigrator {
    parser: UniversalDateParser,
}

impl DataMigrator {
    fn new() -> Self {
        Self {
            parser: UniversalDateParser::new(TimezoneMode::ConvertToUtc),
        }
    }
    
    fn migrate_date_column(&self, values: Vec<String>) -> Vec<String> {
        values.into_iter()
            .map(|date_str| {
                match self.parser.parse(&date_str) {
                    Ok(parsed) => {
                        // ISO 8601 UTC形式に統一
                        format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
                            parsed.year,
                            parsed.month,
                            parsed.day,
                            parsed.hour.unwrap_or(0),
                            parsed.minute.unwrap_or(0),
                            parsed.second.unwrap_or(0)
                        )
                    },
                    Err(_) => date_str, // 解析失敗時は元の値を保持
                }
            })
            .collect()
    }
}

API レスポンス正規化

use universal_date_parser::{UniversalDateParser, TimezoneMode};
use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize)]
struct ApiResponse {
    id: u64,
    #[serde(deserialize_with = "parse_flexible_date")]
    created_at: String,
    data: serde_json::Value,
}

fn parse_flexible_date<'de, D>(deserializer: D) -> Result<String, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let s = String::deserialize(deserializer)?;
    let parser = UniversalDateParser::new(TimezoneMode::ConvertToUtc);
    
    match parser.parse(&s) {
        Ok(parsed) => Ok(format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
            parsed.year,
            parsed.month,
            parsed.day,
            parsed.hour.unwrap_or(0),
            parsed.minute.unwrap_or(0),
            parsed.second.unwrap_or(0)
        )),
        Err(_) => Err(serde::de::Error::custom("無効な日付フォーマット")),
    }
}

エラーハンドリングとデバッグ

エラータイプ

#[derive(Debug, Clone)]
pub enum ParseError {
    InvalidFormat(String),
    InvalidDate(String),
    AmbiguousFormat(String),
    TimezoneParseError(String),
    NumericParseError(String),
}

impl std::fmt::Display for ParseError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            ParseError::InvalidFormat(msg) => write!(f, "無効なフォーマット: {}", msg),
            ParseError::InvalidDate(msg) => write!(f, "無効な日付: {}", msg),
            ParseError::AmbiguousFormat(msg) => write!(f, "曖昧なフォーマット: {}", msg),
            ParseError::TimezoneParseError(msg) => write!(f, "タイムゾーンエラー: {}", msg),
            ParseError::NumericParseError(msg) => write!(f, "数値変換エラー: {}", msg),
        }
    }
}

デバッグモード

let parser = UniversalDateParser::builder()
    .debug_mode(true)
    .build();

match parser.parse("ambiguous/date/format") {
    Ok(result) => println!("成功: {:?}", result),
    Err(e) => {
        println!("エラー: {}", e);
        // デバッグモードでは詳細な解析ステップが出力される
    }
}

テストとベンチマーク

単体テスト例

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_iso_8601_parsing() {
        let parser = UniversalDateParser::new(TimezoneMode::AssumeUtc);
        let result = parser.parse("2023-12-25T10:30:00Z").unwrap();
        
        assert_eq!(result.year, 2023);
        assert_eq!(result.month, 12);
        assert_eq!(result.day, 25);
        assert_eq!(result.hour, Some(10));
        assert_eq!(result.minute, Some(30));
        assert_eq!(result.second, Some(0));
    }

    #[test]
    fn test_ambiguous_format_resolution() {
        let parser = UniversalDateParser::builder()
            .ambiguity_resolver(AmbiguityResolver::PreferEuropean)
            .build();
        
        let result = parser.parse("01/02/2023").unwrap();
        assert_eq!(result.month, 2); // ヨーロッパ式:1日2月
        assert_eq!(result.day, 1);
    }
}

パフォーマンステスト

use criterion::{black_box, criterion_group, criterion_main, Criterion};

fn benchmark_parsing(c: &mut Criterion) {
    let parser = UniversalDateParser::new(TimezoneMode::AssumeUtc);
    
    c.bench_function("ISO 8601 parsing", |b| {
        b.iter(|| parser.parse(black_box("2023-12-25T10:30:00Z")))
    });
    
    c.bench_function("Unix timestamp parsing", |b| {
        b.iter(|| parser.parse(black_box("1703520645")))
    });
}

criterion_group!(benches, benchmark_parsing);
criterion_main!(benches);

よくある質問とトラブルシューティング

Q: 曖昧な日付フォーマットはどのように処理されますか?

A: ライブラリは以下の戦略で曖昧性を解決します:

  1. 文脈分析: 同じ入力内の他の日付パターンから推測
  2. 設定可能な解決方法: AmbiguityResolverで地域設定を指定
  3. エラー報告: 解決不可能な場合は明確なエラーメッセージ
// 曖昧性解決の設定例
let parser = UniversalDateParser::builder()
    .ambiguity_resolver(AmbiguityResolver::PreferUS)  // MM/DD/YYYY優先
    .build();

Q: カスタム日付フォーマットを追加できますか?

A: 現在のバージョンではカスタムフォーマットの直接追加はサポートしていませんが、将来のバージョンで実装予定です。回避策として前処理で標準フォーマットに変換してください。

Q: パフォーマンスを最大化するには?

A: 以下の最適化を推奨します:

  1. 適切なタイムゾーンモード: AssumeUtcが最高速
  2. パーサーの再利用: インスタンス作成コストを削減
  3. バッチ処理: 大量データの一括処理
// 最適化された使用例
let parser = UniversalDateParser::new(TimezoneMode::AssumeUtc);
let dates: Vec<&str> = vec![/* 大量の日付文字列 */];

let results: Vec<_> = dates.par_iter()  // rayon使用で並列処理
    .map(|date| parser.parse(date))
    .collect();

Q: メモリ使用量を削減するには?

A: ライブラリは既に最小限のメモリ使用量で設計されていますが、さらに削減するには:

  1. 不要な文字列クローンを避ける
  2. パース結果の即座の処理
  3. 大量データの場合はストリーミング処理

今後の開発予定

バージョン 0.2.0 (予定)

  • 自然言語解析: "yesterday", "next week"などの相対日付
  • カスタムフォーマット: ユーザー定義パターンのサポート
  • ローカライゼーション: 多言語の月名・曜日名対応

バージョン 0.3.0 (予定)

  • 時間範囲解析: "from 2023-01-01 to 2023-12-31"
  • ファジーマッチング: 軽微な入力エラーの自動修正
  • パフォーマンス向上: SIMD命令の活用

長期計画

  • 機械学習: AIベースのフォーマット推測
  • プラグインシステム: サードパーティ拡張のサポート
  • ビジュアルツール: 解析ルールの可視化

コントリビューション

開発に参加するには

  1. リポジトリのフォーク
  2. 機能ブランチの作成: git checkout -b feature/amazing-feature
  3. 変更のコミット: git commit -m 'Add amazing feature'
  4. ブランチのプッシュ: git push origin feature/amazing-feature
  5. プルリクエストの作成

コーディングガイドライン

  • Rustfmt: cargo fmtでフォーマット
  • Clippy: cargo clippyで静的解析
  • テスト: すべての新機能にテストを追加
  • ドキュメント: パブリックAPIにはドキュメントコメント必須

バグレポート

バグを発見した場合は、以下の情報を含めてIssueを作成してください:

  • Rustバージョン
  • ライブラリバージョン
  • 入力データの例
  • 期待される結果
  • 実際の結果
  • エラーメッセージ

まとめ

Universal Date Parserは、Rustエコシステムにおける包括的な日付解析ソリューションです。高性能、柔軟性、使いやすさを兼ね備え、様々なアプリケーションでの日付処理を大幅に簡素化します。

主な利点

  • 統一されたAPI: 複数フォーマットを単一インターフェースで処理
  • 高性能: 本番環境で実証済みの速度
  • 信頼性: 包括的なテストスイートによる品質保証
  • 拡張性: 将来の機能追加に対応したアーキテクチャ

適用分野

  • ログ解析システム
  • データ移行ツール
  • API統合
  • リアルタイム処理
  • バッチ処理アプリケーション

Rustの安全性とパフォーマンスを活用し、日付解析における複雑さを隠蔽しながら、開発者に強力で使いやすいツールを提供します。


プロジェクトリンク:

ライセンス: MIT / Apache 2.0 デュアルライセンス

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?