Рекомендуемый способ разбора дат, представленных в различных форматах

У меня есть набор дат в виде строк, введенных пользователями за определенный период времени. Поскольку они исходили от людей с небольшой проверкой или без нее, форматы, вводимые для дат, сильно различаются. Ниже приведены некоторые примеры (ведущие числа приведены только для справки):

  1. 20, 21 августа 1897 г.
  2. 31 мая, 1 июня 1909 г.
  3. 29 января 2007 г.
  4. 10, 11, 12 мая 1954 г.
  5. 26, 27, 28, 29, 30 марта 2006 г.
  6. 27, 28, 29, 30 ноября, 1 декабря 2006 г.

Я хотел бы проанализировать эти даты в С #, чтобы получить наборы объектов DateTime с одним объектом DateTime в день. Таким образом, (1) выше приведет к 2 объектам DateTime, а (6) приведет к 5 объектам DateTime.


person Community    schedule 17.02.2011    source источник
comment
Всегда ли он идет в порядке дня, месяца, года?   -  person jamiegs    schedule 17.02.2011
comment
@Jamiegs: Учитывая, что это вводит пользователь, я бы не делал каких-либо общих предположений или прыжков веры.   -  person Brad Christie    schedule 17.02.2011
comment
Не мог не поиграть с этим, пожалуйста, посмотрите мой обновленный ответ.   -  person Brad Christie    schedule 17.02.2011


Ответы (2)


Я бы рекомендовал обработать их для обобщения (в основном удалить числа и имена и сделать их заполнителями), а затем сгруппировать по аналогичному формату, чтобы у вас была группа образцов для работы.

Например, 20th, 21st August 1987 затем становится [number][postfix], [number][postfix] [month] [year] (учитывая, что <number><st|th|rd|nd> распознается как число и постфикс, а месяцы очевидны, а годы - это 4-значные числа).

Оттуда вы узнаете, сколько из них следует этому шаблону, а затем определите, сколько уникальных шаблонов вам нужно сопоставить. Затем у вас может быть хотя бы образец для тестирования любого алгоритма, который вы хотите использовать в нем (регулярное выражение, вероятно, будет вашим лучшим выбором, поскольку оно может обнаруживать повторяющиеся шаблоны (#th[, $th[, ...]]) и названия дней.)


Похоже, вы, вероятно, захотите разбить его по шаблону (учитывая то, что вы предоставили). Так, например, первая выходная информация за год:

(.*?)([0-9]{4})(?:, |$)

Затем вам нужно разбить его на месяцы

(.*?)(January|February|...)(?:, |$)

Тогда вам нужны дни, содержащиеся в этом месяце:

(?:([0-9]{1,2})(?:st|nd|rd|th)(?:, )?)*(?:, |$)

Тогда речь идет о сборе информации. Но опять же, это просто использование того, что у вас есть передо мной. В конечном итоге вам нужно знать, с какими данными вы работаете и как вы хотите их обрабатывать.


Обновлено

Итак, я не мог не попытаться решить эту проблему самостоятельно. Я хотел удостовериться, что метод, который я использовал, был в некоторой степени точным, и я не пускал дым в твою юбку. Сказав это, это то, что я придумал. Обратите внимание, что это в PHP по нескольким причинам:

  1. Мне было легче достать PHP
  2. Я чувствовал, что если это было жизнеспособным решением, вам придется поработать над его переносом. :ухмылка:

В любом случае, вот исходный код и демонстрационный вывод. Наслаждаться.

<?php
  $samples = array(
    '20th, 21st August 1897',
    '31st May, 1st June 1909',
    '29th January 2007',
    '10th, 11th, 12th May 1954',
    '26th, 27th, 28th, 29th, 30th March 2006',
    '27th, 28th, 29th, 30th November, 1st December 2006',
    '30th, 31st, December 2010, 1st, 2nd January 2011'
  );

  //header('Content-Type: text/plain');

  $months = array('january','february','march','april','may','june','july','august','september','october','november','december');

  foreach ($samples as $sample)
  {
    $dates = array();

    // find yearly information first
    $yearly = null;
    if (preg_match_all('/(?:^|\s)(?<month>.*?)\s?(?<year>[0-9]{4})(?:$|,)/',$sample,$yearly))
    {//var_dump($yearly);
      for ($y = 0; $y < count($yearly[0]); $y++)
      {
        $year = $yearly['year'][$y];
        //echo "year: {$year}\r\n";

        $monthly = null;
        if (preg_match_all('/(?<days>(?:(?:^|\s)[0-9]{1,2}(?:st|nd|rd|th),?)*)\s?(?<month>'.implode('|',$months).')$/i',$yearly['month'][$y],$monthly))
        {//var_dump($monthly);
          for ($m = 0; $m < count($monthly[0]); $m++)
          {
            $month = $monthly['month'][$m];
            //echo "month: {$month}\r\n";

            $daily = null;
            if (preg_match_all('/(?:^|\s)(?<day>[0-9]{1,2})(?:st|nd|rd|th)(?:,|$)/i',$monthly['days'][$m],$daily))
            {//var_dump($daily);
              for ($d = 0; $d < count($daily[0]); $d++)
              {
                $day = $daily['day'][$d];
                //echo "day: {$day}\r\n";

                $dates[] = sprintf("%d-%d-%d", array_search(strtolower($month),$months)+1, $day, $year);
              }
            }
          }
        }
        $data = $yearly[1];
      }
    }

    echo "<p><b>{$sample}</b> was parsed to include:</p><ul>\r\n";
    foreach ($dates as $date)
      echo "<li>{$date}</li>\r\n";
    echo "</ul>\r\n";
  }
?>

20th, 21st August 1897 was parsed to include:

  • 8-20-1897
  • 8-21-1897

31 мая, 1 июня 1909 г. был проанализирован и включил:

  • 6-1-1909

29 января 2007 г. был проанализирован, чтобы включить:

  • 1-29-2007

10th, 11th, 12th May 1954 was parsed to include:

  • 5-10-1954
  • 5-11-1954
  • 5-12-1954

26, 27, 28, 29, 30 марта 2006 г. был проанализирован, чтобы включить:

  • 3-26-2006
  • 3-27-2006
  • 3-28-2006
  • 3-29-2006
  • 3-30-2006

27th, 28th, 29th, 30th November, 1st December 2006 was parsed to include:

  • 12-1-2006

30th, 31st, December 2010, 1st, 2nd January 2011 was parsed to include:

  • 12-30-2010
  • 12-31-2010
  • 1-1-2011
  • 1-2-2011

И чтобы доказать, что у меня ничего нет в рукаве, http://www.ideone.com/GGMaH

person Brad Christie    schedule 17.02.2011
comment
Это действительно замечательный ответ - спасибо. Если пользователи вводят месяцы в виде сокращений, я всегда могу добавить их в массив месяцев (например, январь, февраль, март). Незначительный момент - в приведенном выше выводе 31 мая пропущено в строке 31 мая, 1 июня 1909 года. Это опечатка? - person ; 17.02.2011
comment
@ Андрей: Сделал это перед сном, так что ошибки неизбежны. Ошибка во втором регулярном выражении; это не повторение совпадений, а захват последнего совпадения. - person Brad Christie; 17.02.2011

Я подумал еще немного, и решение стало очевидным. Обозначьте строку и проанализируйте токены в обратном порядке. Будет извлечен год, затем месяц, а затем день (а). Вот мое решение:

// **** Start definition of the class bcdb_Globals ****
public static class MyGlobals
{
    static Dictionary<string, int> _month2Int = new Dictionary<string, int>
    {
        {"January", 1},
        {"February", 2},
        {"March", 3},
        {"April", 4},
        {"May", 5},
        {"June", 6},
        {"July", 7},
        {"August", 8},
        {"September", 9},
        {"October", 10},
        {"November", 11},
        {"December", 12}
    };
    static public int GetMonthAsInt(string month)
    {
        return( _month2Int[month] );
    }
}


public class MyClass
{
    static char[] gDateSeparators = new char[2] { ',', ' ' };

    static Regex gDayRegex = new Regex("[0-9][0-9]?(st|nd|rd|th)");
    static Regex gMonthRegex = new Regex("January|February|March|April|May|June|July|August|September|October|November|December");
    static Regex gYearRegex = new Regex("[0-9]{4}");

    public void ParseMatchDate(string matchDate)
    {
        Stack matchDateTimes = new Stack();
        string[] tokens = matchDate.Split(gDateSeparators,StringSplitOptions.RemoveEmptyEntries);
        int curYear = int.MinValue;
        int curMonth = int.MinValue;
        int curDay = int.MinValue;

        for (int pos = tokens.Length-1; pos >= 0; --pos)
        {
            if (gYearRegex.IsMatch(tokens[pos]))
            {
                curYear = int.Parse(tokens[pos]);
            }
            else if (gMonthRegex.IsMatch(tokens[pos]))
            {
                curMonth = MyGlobals.GetMonthAsInt(tokens[pos]);
            }
            else if (gDayRegex.IsMatch(tokens[pos]))
            {
                string tok = tokens[pos];
                curDay = int.Parse(tok.Substring(0,(tok.Length-2)));
                // Dates are in reverse order, so using a stack means we'll pull em off in the correct order
                matchDateTimes.Push(new DateTime(curYear, curMonth, curDay));
            }
        }

        // Now get the datetimes
        while (matchDateTimes.Count > 0)
        {
            // Do something with dates here
        }
    }

}

person Community    schedule 19.02.2011