[Flutter/Dart] 정규표현식을 사용한 문자열 검출 및 범위값 파싱

2021. 9. 13. 13:27Programming/Flutter

반응형

변수를 사용한 정규표현식의 활용

이번에는 정규표현식을 활용해서 1과 0으로 이루어진 문자열을 압축하는(?) 로직을 작성해보자. 나는 데이터가 기록된 문자열에서 시간을 파싱할 때 이 방법을 사용했었다. 예를 들어서 00시부터 24시까지의 문자열이 주어진다고 치면, 전체 문자열의 길이는 144자이며 한 문자는 10분의 단위시간을 가지게 된다. 정규표현식을 사용하여 문자열이 매칭되는 시작 지점과 끝 지점의 인덱스를 알아내면, 문자열로부터 데이터가 위치한 시간을 계산해낼 수 있는 셈이다.

아무튼 오늘 해볼 것은 0과 1로 이루어진 문자열에서 match를 사용하여 시작 지점과 끝 지점을 구하고, 구간 형태의 데이터로 분리해보자. 뭐, 딱히 쓸데는 없다. ' ㅈ';

문자열 만들기

우선은 0과 1로 구성된 문자열을 만들어보자. 간단하게 0000111100001111과 같은 형태로 문자열을 만드는 것도 좋지만, 이번에는 dart:math 패키지에 있는 Random을 사용해서 임의의 문자열을 만들어보자.

import 'dart:math';

Random random = Random();
String schedule = List.generate(144, (index) => random.nextInt(2)).join();

이걸로 144자의 0과 1로 만들어진 문자열이 만들어졌다. 이걸로 0000000111111처럼 연속된 0과 1로 이루어진 문자열과 결과를 비교해보면, 이 압축 결과물(?)의 효과를 좀 더 시각적으로 볼 수 있지 않을까.

정규표현식과 값으로 이루어진 클래스 만들기

다음은 정규표현식과 값으로 이루어진 클래스를 만들어보자. 별다른 건 아니고 문자열에서 0이나 1로 이루어진 문자열을 검출하기 위해서는 r'([0]+)', r'([1]+)'과 같은 정규 표현식을 선언해줘야한다. 지금은 검출해야하는 값이 0과 1 둘 뿐이니 단순 선언하더라도 상관없지만, 검출해야하는 값이 늘어난다면 상당히 귀찮은 일이 될 것이다. 게다가 검출할 때 for([초기값];[조건];[증감문]) 형식으로 만들면 index를 기반으로 값을 산출하는 게 가능하긴 하지만, 21세기가 시작된지 어느덧 21년이 지난 마당에 컬렉션에서 제공해주는 메서드를 사용하지 않는 건 어쩐지 손해보는 느낌이다. 그런 이유로 각 구간과 구간의 값을 저장하기 위한 클래스를 만들어주자.

class RegExpWithValue<T> {
  late final RegExp regexp;
  final T value;

  RegExpWithValue(this.value) {
      regexp = RegExp(r'([' + value.toString() + ']+)');
  }
}

위의 코드를 살펴보자. 생성자에서 받은 값을 기반으로 정규표현식을 생성하므로, _regexp값은 late로 선언해준다. _regexp와 value의 값은 변경될 일이 없으므로 final로 선언했다.

List<RegExpWithValue> list = List.generate(
  2,
  (index) => 
  RegExpWithValue(index)
);

이제 위에서 작성한 데이터 클래스 RegExpWithValue를 기반으로 List<RegExpWithValue>를 생성하자. 이 정규표현식 리스트를 순회하면서, 문자열로부터 값을 검출하면 된다.

구간에 대한 값을 저장하는 데이터 클래스 만들기

알고리즘 문제였다면 검출하고자 하는 값 0, 1과 길이값을 붙여서 하나의 문자열로 만들었을 것이다. 00001111이라면 0414로 나타내는 식으로 말이다. 애시당초 그럴거면 정규표현식을 쓰는 것 보다 문자열을 처음부터 끝까지 순회하는게 더 낫기 때문에... 이번에는 시작 위치, 끝 위치, 저장된 값으로 구성된 클래스를 만들어서 값을 저장하도록 하자.

class Zip {
  final int start;
  final int end;
  final int value;

  const Zip(this.start, this.end, this.value);
}

너무 심플해서 별도로 설명할 필요도 없다.

정규표현식 리스트를 순회하면서 값 검출하기

위에서 만든 정규표현식 리스트 list를 순회하면서 값을 검출하여 Zip 객체로 파싱하고, 완성된 List<Zip>을 start값을 기준으로 정렬하는 코드다.

List<Zip> result = list.fold<List<Zip>>([], (cons, 

regexpWithValue) {
  cons.addAll(regexpWithValue.regexp.allMatches(schedule).map((match) => Zip(match.start, match.end, regexpWithValue.value)));
  return cons;
})
..sort((zipA, zipB) => zipA.start - zipB.start);


for (var zip in result) {
  print("${zip.start}~${zip.end}: ${zip.value}");
}

마지막에서는 for...in을 사용하여 List<Zip>을 순회하며 내용을 출력하고 있다.

정리

전체 코드는 아래와 같다.

import 'dart:math';

class Zip {
  final int start;
  final int end;
  final int value;

  const Zip(this.start, this.end, this.value);
}

class RegExpWithValue<T> {
  late final RegExp regexp;
  final T value;

  RegExpWithValue(this.value) {
      regexp = RegExp(r'([' + value.toString() + ']+)');
  }
}

void main() {
  Random random = Random();
  String schedule = List.generate(144, (index) => random.nextInt(2)).join();

  List<RegExpWithValue> list = List.generate(
    2,
    (index) => 
    RegExpWithValue(index)
  );

  List<Zip> result = list.fold<List<Zip>>([], (cons, regexpWithValue) {
    cons.addAll(regexpWithValue.regexp.allMatches(schedule).map((match) => Zip(match.start, match.end, regexpWithValue.value)));
    return cons;
  })
    ..sort((zipA, zipB) => zipA.start - zipB.start);


  for (var zip in result) {
    print("${zip.start}~${zip.end}: ${zip.value}");
  }
}

이번 글에서 정규표현식을 사용하는 방법은 사실상 뻘짓에 가깝지만, 정규표현식을 사용해서 문자열을 검출하는 건 생각보다 쉽고, 편리하며, 여러가지로 도움이 된다. RegExp.match, RegExp.allMatch를 사용하여 검출하고자 하는 문자열의 start, end값을 얻은 다음 다른 형태로 데이터를 가공할 수 있다는 점만 짚고 넘어가도록 하자. ' ㅂ')

반응형