열네번째 날: CPAN에서 만남을 추구하면 안되는 걸까 Vol.2

저자

@JEEN_LEE - 아들러 심리학 맹독중, 딸러도 좋아함

시작하며

업무에서 가장 빈번하게 접하는 데이터는 바로 날짜입니다. 날짜라면 단순한 데이터라고 할 수 있을텐데 현실 세계는 조금 더 잔인한 법입니다. 이런저런 사람들이 쏟아내는 날짜 형식이라고 부르는 데이터들은 아래와 같습니다.

2015-12-11
2015-12-11 13:00
2015-12-11 13:00:00
2015-12-11T13:00:00Z
2015-12-11T13:00:00.00
2015/12/11
20151211
20151211130000
2015년 12월 11일
...

그렇다면 우리는 어떤 자세로 이런 데이터들을 대해야 할까요?

1. 정규표현식

그렇습니다. 만고불변의 @JEEN_LEE인 정규표현식을 사용하는 것입니다.

my $date_str = "2015-12-11";
my ( $y, $m, $d ) = $str = ~ /^([\d]{4})-([\d]{2})-([\d]{2})$/;

하지만 위 코드의 약점을 아실 겁니다. 그건 바로... 이런 거죠.

Month knuckle trick 그림 1. Month knuckle trick (원본)

2월은 28일, 29일이 있고, 매달 올록볼록 엠보싱처럼 30일, 31일이 왔다합니다. 정규표현식으로 이런 것을 짜맞추는 것이 그렇게 어렵지는 않습니다. 그냥 그 규칙들을 생각하면서 차분히 정규표현식을 만들면 되겠죠. 이렇게 말입니다. :)

(?^:(?^:2015-(?:0(?:2-(?:0[123456789]|2[012345678]|1\d)|1-(?:0[123456789]|3[01]|1\d|2\d)|3-(?:0[123456789]|3[01]|1\d|2\d)|5-(?:0[123456789]|3[01]|1\d|2\d)|7-(?:0[123456789]|3[01]|1\d|2\d)|8-(?:0[123456789]|3[01]|1\d|2\d)|4-(?:0[123456789]|1\d|2\d|30)|6-(?:0[123456789]|1\d|2\d|30)|9-(?:0[123456789]|1\d|2\d|30))|1(?:0-(?:0[123456789]|3[01]|1\d|2\d)|2-(?:0[123456789]|3[01]|1\d|2\d)|1-(?:0[123456789]|1\d|2\d|30)))))

자, 위의 2015년 전용 날짜 정규표현식을 넣고 유효성까지 확인하면 깔끔하게 데이터를 얻을 수 있겠죠?

괜찮은데? 그림 2. 괜찮은데? (원본)

2. 날짜 형식에 맞는 파싱 모듈을 사용

조금 덩치가 크긴 하지만 세상에는 DateTime이라는 이름 그대로 날짜와 시간을 다루는 가장 유명한 모듈이 있습니다. 이 DateTimeDateTime::Format::* 포맷팅 모듈을 사용해서 각각 데이터 형식에 맞는 파싱모듈을 준비합니다. 포맷팅에 맞는 결과들은 모두 DateTime 객체로 반환되니까 코드의 일관성을 유지하기 좋습니다.

2015-12-12 00:00:00과 같은 형식이면 DateTime::Format::MySQL과 같은 모듈을 사용하고, 20151212000000과 같은 형식이면 DateTime::Format::D?????과 같은 모듈을 사용하도록 if-elsif-elsif-elsif-elsif-elsif의 향연을 펼치는 겁니다.

3. 포맷 빌더를 사용해서 자체적인 포맷팅 모듈 만들기

대개의 DateTime::Format::* 모듈들이 사용하고 있는 방법들입니다.

package DateTime::Format::Jeenlee;

use DateTime::Format::Builder
(
    parsers => {
        parse_datetime => [
            {
                params => [qw( year month day hour minute second )],
                regex  => qr/^...blahblah...$/,
            },
            { ... },
        ],
    },
);

parse_datetime의 배열 레퍼런스 안에 위의 저 많은 케이스를 우겨넣는거죠.

정리하...

어떤가요? 이제 쉽게(!) 날짜를 다룰 수 있을 것 같죠? 어떤 날짜 데이터도 무섭지 않습니다.

...

바로 이 모듈과 함께라면 말이죠.

준비물

필요한 모듈은 다음과 같습니다.

직접 CPAN을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.

$ sudo cpan DateTime::Format::Flexible

사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나 perlbrew를 이용해서 자신만의 Perl을 사용하고 있다면 다음 명령을 이용해서 모듈을 설치합니다.

$ cpan DateTime::Format::Flexible

이제부터 진면목

놀랍게도 DateTime::Format::Flexible 하나로 처음에 제시한 여러가지 날짜 데이터를 단일코드로 처리할 수 있습니다.

use v5.22;

use DateTime::Format::Flexible;

my @dates = (
    "2015-12-11",
    "2015-12-11 00:00",
    "2015-12-11 00:00:00",
    "2015-12-11T00:00:00",
    "2015/12/11",
    "20151211",
    "20151211000000"
);

for my $date_str (@dates) {
    my $dt = DateTime::Format::Flexible->parse_datetime($date_str);
    say $dt;
}

뿐만 아니라 어느 정도 자연스러운 날짜 표현도 어느 정도 다룰 수 있습니다.

now/tomorrow/yesterday
2 months ago/2 days ago/ 2 minutes ago
...

parse_datetime 인자로 앞의 예제 값을 넣으면 그에 맞는 DateTime 객체가 반환됩니다.

DateTime::Format::Flexible->parse_datetime('2 hours ago');

그리고 이 모듈은 DateTime::Format::Flexible::lang::*로 각 언어별 확장의 여지를 남겨두고 있습니다. DateTime::Format::Flexible::lang::ko 같은 걸 만들어서 올려놔도 되겠죠?

package DateTime::Format::Flexible::lang::ko;

use utf8;
use strict;
use warnings;

sub new {
    my ( $class, %params ) = @_;
    my $self = bless \%params, $class;
    return $self;
}

sub months {
    return (
        qr{1월}  => 1,
        qr{2월}  => 2,
        qr{3월}  => 3,
        qr{4월}  => 4,
        qr{5월}  => 5,
        qr{6월}  => 6,
        qr{7월}  => 7,
        qr{8월}  => 8,
        qr{9월}  => 9,
        qr{10월} => 10,
        qr{11월} => 11,
        qr{12월} => 12,
    );
}

sub days {
    return (
        qr{\b월요일\b} => 1,    # Monday
        qr{\b화요일\b} => 2,    # Tuesday
        qr{\b수요일\b} => 3,    # Wednesday
        qr{\b목요일\b} => 4,    # Thursday
        qr{\b금요일\b} => 5,    # Friday
        qr{\b토요일\b} => 6,    # Saturday
        qr{\b일요일\b} => 7,    # Sunday
    );
}

sub day_numbers {
    return (
        qr{1일}  => 1,
        qr{2일}  => 2,
        qr{3일}  => 3,
        qr{4일}  => 4,
        qr{5일}  => 5,
        qr{6일}  => 6,
        qr{7일}  => 7,
        qr{8일}  => 8,
        qr{9일}  => 9,
        qr{10일} => 10,
        qr{11일} => 11,
        qr{12일} => 12,
        qr{13일} => 13,
        qr{14일} => 14,
        qr{15일} => 15,
        qr{16일} => 16,
        qr{17일} => 17,
        qr{18일} => 18,
        qr{19일} => 19,
        qr{20일} => 20,
        qr{21일} => 21,
        qr{22일} => 22,
        qr{23일} => 23,
        qr{24일} => 24,
        qr{25일} => 25,
        qr{26일} => 26,
        qr{27일} => 27,
        qr{28일} => 28,
        qr{29일} => 29,
        qr{30일} => 30,
        qr{31일} => 31,
    );
}

sub hours {
    return (
        '정오' => '12:00:00',
        '자정' => '00:00:00',
    );
}

sub remove_strings {
    return ();
}

sub parse_time {
    my ( $self, $date ) = @_;
    $date =~ s/(\d+)(?:년|월|일)/$1/g;
    $date =~ s/[Xn ]//g;
    return $date;
}

sub string_dates {
    my $base_dt = DateTime::Format::Flexible->base;
    return (
        '지금'     => sub { return $base_dt->datetime },
        '오늘'     => sub { return $base_dt->clone->truncate( to => 'day' )->ymd },
        '내일'     => sub { return $base_dt->clone->truncate( to => 'day' )->add( days => 1 )->ymd; },
        '어제'     => sub { return $base_dt->clone->truncate( to => 'day' ) ->subtract( days => 1 )->ymd; },
        '모레'     => sub { return DateTime->today->add( days => 2 )->ymd },
        '내일모레' => sub { return DateTime->today->add( days => 2 )->ymd },
        '글피'     => sub { return DateTime->today->add( days => 3 )->ymd },
    );
}

sub ago {
    return qr{\b전\b}i;
}

sub math_strings {
    return (
        '년'   => 'years',
        '월'   => 'months',
        '개월' => 'months',
        '일'   => 'days',
        '시'   => 'hours',
        '시간' => 'hours',
        '분'   => 'minutes',
    );
}

sub timezone_map {
    return ( KST => 'Asia/Seoul', );
}

네, 그래서 한번 끄적거려 봤습니다. 이걸 사용해서 한글 날짜 데이터를 받아서, 아래와 같이 사용할 수 있습니다.

DateTime::Format::Flexible->parse_datetime(
    '2 개월 전',
    lang => ['ko'],
); # 2016년 12월 11일/지금/내일/...

보름 전이나 이틀 후 같은 다양한 표현도 받아들일 수 있게끔 하는 것도 괜찮겠다 싶습니다만, 아직은 그런 것들은 아니됩니다. 좀 더 확장의 여지가 남아있네요. 도전과제로 삼아보는 것도 좋을 듯 합니다.

정리하며

어떤가요? 뭔가 내 일자리를 CPAN에게 뺏기는 것은 아닌가 하는 생각이 들지 않나요?

사실 위의 DateTime::Format::Flexible::lang::ko는 그냥 대충 기사 써내기 위해서 대충 형식만 맞춘 것입니다. 좀 더 다듬어서 모듈 저자에게 패치를 보내는 것도 좋겠죠. :-)

blog comments powered by Disqus