Идентификатор последней даты предыдущих месяцев

В моем кадре данных есть DateId (т.е. целочисленный столбец, определяющий дату как количество дней с 1993-06-25). Цель состоит в том, чтобы вычислить идентификатор даты последнего дня месяца до каждой даты в столбце:

DateId ->  _intermittent calc Date_       -> _result LastDayOfPriorMonthId_
9063   ->  2018-04-18                     -> 9045 (i.e. 2018-03-31)
8771   ->  2017-06-30                     -> 8741 (i.e. 2017-05-31)
9175   ->  2018-08-08                     -> 9167 (i.e. 2018-07-31)

Решение было бы очень простым, но у меня возникают проблемы с преобразованием типов:

val a = Seq(9063, 8771, 9175).toDF("DateId")
val timeStart = to_date(lit("1993-06-25"))


val dateIdAdd : (Column) => Column = x => {x - date_add(timeStart, x).DATE_OF_MONTH}

Компиляция функции завершается со следующей ошибкой:

notebook:2: error: type mismatch;
found   : org.apache.spark.sql.Column
required: Int
   x - date_add(timeStart, x).DATE_OF_MONTH

Такие выражения, как .cast(IntegerType), не меняют результат (x по-прежнему является искровым типом столбца, а .cast(Int) неприменим.

Обратите внимание: аналогичная проблема была решена в этом ТАК вопрос, но тот же подход терпит неудачу, когда здесь применяется константа timeStart. Также использование функции было бы предпочтительнее, чем выражение, потому что в одном и том же вычислении используется несколько столбцов с реальными данными.


person Dan    schedule 25.07.2018    source источник


Ответы (2)


Можете ли вы перевести с Java? Извините, я не программирую на Scala (пока).

private static final LocalDate baseDate = LocalDate.of(1993, Month.JUNE, 25);

public static long dateIdAdd(long dateId) {
    LocalDate date = baseDate.plusDays(dateId);
    LocalDate lastOfPrevMonth = YearMonth.from(date).minusMonths(1).atEndOfMonth();
    return ChronoUnit.DAYS.between(baseDate, lastOfPrevMonth);
}

Редактировать: согласно вам (Дэн, спрашивающий), версия Scala:

val baseDate = LocalDate.of(1993, Month.JUNE, 25)
val lastDayIdOfPriorMonth = udf((dateId : Long) => {
    val date = baseDate.plusDays(dateId)
    val lastOfPrevMonth = YearMonth.from(date).minusMonths(1).atEndOfMonth()
    ChronoUnit.DAYS.between(baseDate, lastOfPrevMonth)
})

Давайте попробуем это с датами вашего примера (снова Java):

    System.out.println("9063 -> " + dateIdAdd(9063));
    System.out.println("8771 -> " + dateIdAdd(8771));
    System.out.println("9175 -> " + dateIdAdd(9175));

Это печатает:

9063 -> 9045
8771 -> 8741
9175 -> 9167

В своем вопросе вы дали 9176 в качестве желаемого результата в последнем случае, но я полагаю, что это была опечатка?

И, пожалуйста, наслаждайтесь тем, насколько ясным и понятным является код.

person Ole V.V.    schedule 26.07.2018
comment
Вы правы насчет опечатки в исходном описании. 9167 - правильный ответ - мой хак на основе текстовой строки дает тот же результат (редактирование исходного вопроса, чтобы избежать путаницы). - person Dan; 27.07.2018
comment
для вашего решения требуются 3 дополнительные библиотеки (import java.time import java.time._ import java.time.temporal.ChronoUnit) версия Scala: val baseDate = LocalDate.of(1993, Month.JUNE, 25) val lastDayIdOfPriorMonth = udf((dateId : Long) => { val date = baseDate.plusDays(dateId) val lastOfPrevMonth = YearMonth.from(date).minusMonths(1).atEndOfMonth() ChronoUnit.DAYS.between(baseDate, lastOfPrevMonth) }) Укажите это решение в своем ответе! - person Dan; 27.07.2018
comment
Спасибо за перевод Scala. Я добавил это к ответу. Так вы считаете библиотеки в Scala? В Java я бы просто сказал, что для этого требуется JDK, то есть никаких дополнительных библиотек. Это требует импорта java.time._ и java.time.temporal.ChronoUnit. - person Ole V.V.; 27.07.2018
comment
Да, по моим ограниченным знаниям, библиотеки должны быть импортированы. Может быть, есть альтернатива, но учебные пособия и примеры регулярно используют импорт (например, docs.azuredatabricks.net/spark/latest/dataframes-datasets/) - person Dan; 27.07.2018
comment
Я думаю, что это больше вопрос использования слов. Классы должны быть импортированы (там, где я работаю, 50 операторов импорта — не такая уж редкость, и мы не рассматриваем это как проблему). Обычно в библиотеке много классов. - person Ole V.V.; 27.07.2018

После тестирования многих вариантов с функцией преобразования Scala, взлома на основе UDF со строкой Java и SimpleDateFormat единственного, что я смог понять:

val dateIdAdd = udf((dateId : Long) => {
   val d = new SimpleDateFormat("yyyy-MM-dd")
   val ts = d.parse("1993-06-25")
   val tc = d.format(new Date(ts.getTime() + (24 * 3600 * 1000 * dateId)))
   dateId - Integer.parseInt(tc.substring(tc.length()-2))
})

После добавления еще одной функции поддержки для проверки и простого выбора:

val dateIdToDate = udf((dateId : Long) => {
  val d = new SimpleDateFormat("yyyy-MM-dd")
  val ts = d.parse("1993-06-25")
  d.format(new Date(ts.getTime() + (24 * 3600 * 1000 * dateId)))
})

val aa = a.select($"*"
             , dateIdToDate($"DateId") as "CalcDateFromId"
             , dateIdAdd($"DateId") as "CalcLastDayOfMonthId")

display(aa)

Генерируются ожидаемые результаты (но я сомневаюсь, что это самый эффективный способ):

DateId  CalcDateFromId  CalcLastDayOfMonthId
9063    4/18/2018       9045
8771    6/30/2017       8741
9175    8/8/2018        9167
person Dan    schedule 26.07.2018