diff --git a/DESCRIPTION b/DESCRIPTION index 43b8d05b2..467bb10ed 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -117,5 +117,5 @@ Collate: 'zzz.R' Config/rextendr/version: 0.3.1 VignetteBuilder: knitr -Config/polars/LibVersion: 0.38.1 +Config/polars/LibVersion: 0.38.2 Config/polars/RustToolchainVersion: nightly-2024-02-23 diff --git a/NEWS.md b/NEWS.md index 28286f5b1..5dd547b13 100644 --- a/NEWS.md +++ b/NEWS.md @@ -8,6 +8,11 @@ character scalars, such as `$drop("a", "b", "c")`. Explicitly using the `columns` argument now errors (#912). +### New features + +- New functions `pl$datetime()`, `pl$date()`, and `pl$time()` to easily create + Expr of class datetime, date, and time via columns and literals (#918). + ## Polars R Package 0.15.1 ### New features diff --git a/R/extendr-wrappers.R b/R/extendr-wrappers.R index d2e54ff8f..5b141e032 100644 --- a/R/extendr-wrappers.R +++ b/R/extendr-wrappers.R @@ -16,6 +16,8 @@ any_horizontal <- function(dotdotdot) .Call(wrap__any_horizontal, dotdotdot) coalesce_exprs <- function(exprs) .Call(wrap__coalesce_exprs, exprs) +datetime <- function(year, month, day, hour, minute, second, microsecond, time_unit, time_zone, ambiguous) .Call(wrap__datetime, year, month, day, hour, minute, second, microsecond, time_unit, time_zone, ambiguous) + duration <- function(weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, time_unit) .Call(wrap__duration, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, time_unit) min_horizontal <- function(dotdotdot) .Call(wrap__min_horizontal, dotdotdot) diff --git a/R/functions__lazy.R b/R/functions__lazy.R index f43b451ea..d49ad6b4e 100644 --- a/R/functions__lazy.R +++ b/R/functions__lazy.R @@ -1047,3 +1047,133 @@ pl_from_epoch = function(column, time_unit = "s") { column$cast(pl$Datetime(time_unit)) ) } + +#' Create a Datetime expression +#' +#' @param year An Expr or something coercible to an Expr, that must return an +#' integer. Strings are parsed as column names. Floats are cast to integers. +#' @param month An Expr or something coercible to an Expr, that must return an +#' integer between 1 and 12. Strings are parsed as column names. Floats are +#' cast to integers. +#' @param day An Expr or something coercible to an Expr, that must return an +#' integer between 1 and 31. Strings are parsed as column names. Floats are +#' cast to integers. +#' @param hour An Expr or something coercible to an Expr, that must return an +#' integer between 0 and 23. Strings are parsed as column names. Floats are +#' cast to integers. +#' @param minute An Expr or something coercible to an Expr, that must return an +#' integer between 0 and 59. Strings are parsed as column names. Floats are +#' cast to integers. +#' @param second An Expr or something coercible to an Expr, that must return an +#' integer between 0 and 59. Strings are parsed as column names. Floats are +#' cast to integers. +#' @param microsecond An Expr or something coercible to an Expr, that must +#' return an integer between 0 and 999,999. Strings are parsed as column +#' names. Floats are cast to integers. +#' @param ... Not used. +#' @inheritParams DataType_Datetime +#' @inheritParams ExprDT_replace_time_zone +#' +#' @return An Expr of type Datetime +#' @seealso +#' - [`pl$date()`][pl_date] +#' - [`pl$time()`][pl_time] +#' +#' @examples +#' df = pl$DataFrame( +#' year = 2019:2021, +#' month = 9:11, +#' day = 10:12, +#' min = 55:57 +#' ) +#' +#' df$with_columns( +#' dt_from_cols = pl$datetime("year", "month", "day", minute = "min"), +#' dt_from_lit = pl$datetime(2020, 3, 5, hour = 20:22), +#' dt_from_mix = pl$datetime("year", 3, 5, second = 1) +#' ) +#' +#' # floats are coerced to integers +#' df$with_columns( +#' dt_floats = pl$datetime(2018.8, 5.3, 1, second = 2.1) +#' ) +#' +#' # if datetime can't be constructed, it returns null +#' df$with_columns( +#' dt_floats = pl$datetime(pl$lit("abc"), -2, 1) +#' ) +#' +#' # can control the time_unit +#' df$with_columns( +#' dt_from_cols = pl$datetime("year", "month", "day", minute = "min", time_unit = "ms") +#' ) +pl_datetime = function(year, month, day, hour = NULL, minute = NULL, second = NULL, microsecond = NULL, ..., time_unit = "us", time_zone = NULL, ambiguous = "raise") { + datetime(year, month, day, hour, minute, second, microsecond, time_unit, time_zone, ambiguous) |> + unwrap("in pl$datetime():") +} + +#' Create a Date expression +#' +#' @inheritParams pl_datetime +#' +#' @return An Expr of type Date +#' @seealso +#' - [`pl$datetime()`][pl_datetime] +#' - [`pl$time()`][pl_time] +#' +#' @examples +#' df = pl$DataFrame(year = 2019:2021, month = 9:11, day = 10:12) +#' +#' df$with_columns( +#' date_from_cols = pl$date("year", "month", "day"), +#' date_from_lit = pl$date(2020, 3, 5), +#' date_from_mix = pl$date("year", 3, 5) +#' ) +#' +#' # floats are coerced to integers +#' df$with_columns( +#' date_floats = pl$date(2018.8, 5.3, 1) +#' ) +#' +#' # if date can't be constructed, it returns null +#' df$with_columns( +#' date_floats = pl$date(pl$lit("abc"), -2, 1) +#' ) +pl_date = function(year, month, day) { + pl$datetime(year, month, day)$cast(pl$Date)$alias("date") |> + result() |> + unwrap("in pl$date():") +} + +#' Create a Time expression +#' +#' @inheritParams pl_datetime +#' +#' @return An Expr of type Time +#' @seealso +#' - [`pl$datetime()`][pl_datetime] +#' - [`pl$date()`][pl_date] +#' +#' @examples +#' df = pl$DataFrame(hour = 19:21, min = 9:11, sec = 10:12, micro = 1) +#' +#' df$with_columns( +#' time_from_cols = pl$time("hour", "min", "sec", "micro"), +#' time_from_lit = pl$time(12, 3, 5), +#' time_from_mix = pl$time("hour", 3, 5) +#' ) +#' +#' # floats are coerced to integers +#' df$with_columns( +#' time_floats = pl$time(12.5, 5.3, 1) +#' ) +#' +#' # if time can't be constructed, it returns null +#' df$with_columns( +#' time_floats = pl$time(pl$lit("abc"), -2, 1) +#' ) +pl_time = function(hour = NULL, minute = NULL, second = NULL, microsecond = NULL) { + pl$datetime(year = 1970, month = 1, day = 1, hour, minute, second, microsecond)$cast(pl$Time)$alias("time") |> + result() |> + unwrap("in pl$time():") +} diff --git a/man/pl_date.Rd b/man/pl_date.Rd new file mode 100644 index 000000000..14a9acafe --- /dev/null +++ b/man/pl_date.Rd @@ -0,0 +1,51 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/functions__lazy.R +\name{pl_date} +\alias{pl_date} +\title{Create a Date expression} +\usage{ +pl_date(year, month, day) +} +\arguments{ +\item{year}{An Expr or something coercible to an Expr, that must return an +integer. Strings are parsed as column names. Floats are cast to integers.} + +\item{month}{An Expr or something coercible to an Expr, that must return an +integer between 1 and 12. Strings are parsed as column names. Floats are +cast to integers.} + +\item{day}{An Expr or something coercible to an Expr, that must return an +integer between 1 and 31. Strings are parsed as column names. Floats are +cast to integers.} +} +\value{ +An Expr of type Date +} +\description{ +Create a Date expression +} +\examples{ +df = pl$DataFrame(year = 2019:2021, month = 9:11, day = 10:12) + +df$with_columns( + date_from_cols = pl$date("year", "month", "day"), + date_from_lit = pl$date(2020, 3, 5), + date_from_mix = pl$date("year", 3, 5) +) + +# floats are coerced to integers +df$with_columns( + date_floats = pl$date(2018.8, 5.3, 1) +) + +# if date can't be constructed, it returns null +df$with_columns( + date_floats = pl$date(pl$lit("abc"), -2, 1) +) +} +\seealso{ +\itemize{ +\item \code{\link[=pl_datetime]{pl$datetime()}} +\item \code{\link[=pl_time]{pl$time()}} +} +} diff --git a/man/pl_datetime.Rd b/man/pl_datetime.Rd new file mode 100644 index 000000000..2560c1870 --- /dev/null +++ b/man/pl_datetime.Rd @@ -0,0 +1,104 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/functions__lazy.R +\name{pl_datetime} +\alias{pl_datetime} +\title{Create a Datetime expression} +\usage{ +pl_datetime( + year, + month, + day, + hour = NULL, + minute = NULL, + second = NULL, + microsecond = NULL, + ..., + time_unit = "us", + time_zone = NULL, + ambiguous = "raise" +) +} +\arguments{ +\item{year}{An Expr or something coercible to an Expr, that must return an +integer. Strings are parsed as column names. Floats are cast to integers.} + +\item{month}{An Expr or something coercible to an Expr, that must return an +integer between 1 and 12. Strings are parsed as column names. Floats are +cast to integers.} + +\item{day}{An Expr or something coercible to an Expr, that must return an +integer between 1 and 31. Strings are parsed as column names. Floats are +cast to integers.} + +\item{hour}{An Expr or something coercible to an Expr, that must return an +integer between 0 and 23. Strings are parsed as column names. Floats are +cast to integers.} + +\item{minute}{An Expr or something coercible to an Expr, that must return an +integer between 0 and 59. Strings are parsed as column names. Floats are +cast to integers.} + +\item{second}{An Expr or something coercible to an Expr, that must return an +integer between 0 and 59. Strings are parsed as column names. Floats are +cast to integers.} + +\item{microsecond}{An Expr or something coercible to an Expr, that must +return an integer between 0 and 999,999. Strings are parsed as column +names. Floats are cast to integers.} + +\item{...}{Not used.} + +\item{time_unit}{Unit of time. One of \code{"ms"}, \code{"us"} (default) or \code{"ns"}.} + +\item{time_zone}{Time zone string, as defined in \code{\link[=OlsonNames]{OlsonNames()}}. +Setting \code{timezone = "*"} will match any timezone, which can be useful to +select all Datetime columns containing a timezone.} + +\item{ambiguous}{Determine how to deal with ambiguous datetimes: +\itemize{ +\item \code{"raise"} (default): raise +\item \code{"earliest"}: use the earliest datetime +\item \code{"latest"}: use the latest datetime +}} +} +\value{ +An Expr of type Datetime +} +\description{ +Create a Datetime expression +} +\examples{ +df = pl$DataFrame( + year = 2019:2021, + month = 9:11, + day = 10:12, + min = 55:57 +) + +df$with_columns( + dt_from_cols = pl$datetime("year", "month", "day", minute = "min"), + dt_from_lit = pl$datetime(2020, 3, 5, hour = 20:22), + dt_from_mix = pl$datetime("year", 3, 5, second = 1) +) + +# floats are coerced to integers +df$with_columns( + dt_floats = pl$datetime(2018.8, 5.3, 1, second = 2.1) +) + +# if datetime can't be constructed, it returns null +df$with_columns( + dt_floats = pl$datetime(pl$lit("abc"), -2, 1) +) + +# can control the time_unit +df$with_columns( + dt_from_cols = pl$datetime("year", "month", "day", minute = "min", time_unit = "ms") +) +} +\seealso{ +\itemize{ +\item \code{\link[=pl_date]{pl$date()}} +\item \code{\link[=pl_time]{pl$time()}} +} +} diff --git a/man/pl_pl.Rd b/man/pl_pl.Rd index 0314de1ab..ed7dda13a 100644 --- a/man/pl_pl.Rd +++ b/man/pl_pl.Rd @@ -6,7 +6,7 @@ \alias{pl} \title{The complete polars public API.} \format{ -An object of class \code{pl_polars_env} (inherits from \code{environment}) of length 95. +An object of class \code{pl_polars_env} (inherits from \code{environment}) of length 98. } \usage{ pl diff --git a/man/pl_time.Rd b/man/pl_time.Rd new file mode 100644 index 000000000..e37dbb3cb --- /dev/null +++ b/man/pl_time.Rd @@ -0,0 +1,56 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/functions__lazy.R +\name{pl_time} +\alias{pl_time} +\title{Create a Time expression} +\usage{ +pl_time(hour = NULL, minute = NULL, second = NULL, microsecond = NULL) +} +\arguments{ +\item{hour}{An Expr or something coercible to an Expr, that must return an +integer between 0 and 23. Strings are parsed as column names. Floats are +cast to integers.} + +\item{minute}{An Expr or something coercible to an Expr, that must return an +integer between 0 and 59. Strings are parsed as column names. Floats are +cast to integers.} + +\item{second}{An Expr or something coercible to an Expr, that must return an +integer between 0 and 59. Strings are parsed as column names. Floats are +cast to integers.} + +\item{microsecond}{An Expr or something coercible to an Expr, that must +return an integer between 0 and 999,999. Strings are parsed as column +names. Floats are cast to integers.} +} +\value{ +An Expr of type Time +} +\description{ +Create a Time expression +} +\examples{ +df = pl$DataFrame(hour = 19:21, min = 9:11, sec = 10:12, micro = 1) + +df$with_columns( + time_from_cols = pl$time("hour", "min", "sec", "micro"), + time_from_lit = pl$time(12, 3, 5), + time_from_mix = pl$time("hour", 3, 5) +) + +# floats are coerced to integers +df$with_columns( + time_floats = pl$time(12.5, 5.3, 1) +) + +# if time can't be constructed, it returns null +df$with_columns( + time_floats = pl$time(pl$lit("abc"), -2, 1) +) +} +\seealso{ +\itemize{ +\item \code{\link[=pl_datetime]{pl$datetime()}} +\item \code{\link[=pl_date]{pl$date()}} +} +} diff --git a/src/rust/Cargo.lock b/src/rust/Cargo.lock index 5e2257e82..142cf6774 100644 --- a/src/rust/Cargo.lock +++ b/src/rust/Cargo.lock @@ -1769,7 +1769,7 @@ dependencies = [ [[package]] name = "r-polars" -version = "0.38.1" +version = "0.38.2" dependencies = [ "either", "extendr-api", diff --git a/src/rust/Cargo.toml b/src/rust/Cargo.toml index 3fd125fe4..90a4c070d 100644 --- a/src/rust/Cargo.toml +++ b/src/rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "r-polars" -version = "0.38.1" +version = "0.38.2" edition = "2021" rust-version = "1.76.0" publish = false diff --git a/src/rust/src/rlib.rs b/src/rust/src/rlib.rs index c4894d7ad..9ab28a1d5 100644 --- a/src/rust/src/rlib.rs +++ b/src/rust/src/rlib.rs @@ -291,12 +291,42 @@ pub fn duration( Ok(polars::lazy::dsl::duration(args).into()) } +#[extendr] +#[allow(clippy::too_many_arguments)] +pub fn datetime( + year: Robj, + month: Robj, + day: Robj, + hour: Robj, + minute: Robj, + second: Robj, + microsecond: Robj, + time_unit: Robj, + time_zone: Robj, + ambiguous: Robj, +) -> RResult { + let args = pl::DatetimeArgs { + year: robj_to!(PLExprCol, year)?, + month: robj_to!(PLExprCol, month)?, + day: robj_to!(PLExprCol, day)?, + hour: robj_to!(Option, PLExprCol, hour)?.unwrap_or(polars::lazy::dsl::lit(0)), + minute: robj_to!(Option, PLExprCol, minute)?.unwrap_or(polars::lazy::dsl::lit(0)), + second: robj_to!(Option, PLExprCol, second)?.unwrap_or(polars::lazy::dsl::lit(0)), + microsecond: robj_to!(Option, PLExprCol, microsecond)?.unwrap_or(polars::lazy::dsl::lit(0)), + time_unit: robj_to!(timeunit, time_unit)?, + time_zone: robj_to!(Option, String, time_zone)?, + ambiguous: robj_to!(PLExpr, ambiguous)?, + }; + Ok(polars::lazy::dsl::datetime(args).into()) +} + extendr_module! { mod rlib; fn all_horizontal; fn any_horizontal; fn coalesce_exprs; + fn datetime; fn duration; fn min_horizontal; fn max_horizontal; diff --git a/tests/testthat/_snaps/after-wrappers.md b/tests/testthat/_snaps/after-wrappers.md index 9ac105d87..f216d3965 100644 --- a/tests/testthat/_snaps/after-wrappers.md +++ b/tests/testthat/_snaps/after-wrappers.md @@ -24,33 +24,34 @@ [37] "concat" "concat_list" [39] "concat_str" "corr" [41] "count" "cov" - [43] "date_range" "disable_string_cache" - [45] "dtypes" "duration" - [47] "element" "enable_string_cache" - [49] "expr_to_r" "first" - [51] "fold" "from_epoch" - [53] "get_global_rpool_cap" "head" - [55] "implode" "is_schema" - [57] "last" "len" - [59] "lit" "max" - [61] "max_horizontal" "mean" - [63] "median" "mem_address" - [65] "min" "min_horizontal" - [67] "n_unique" "numeric_dtypes" - [69] "raw_list" "read_csv" - [71] "read_ndjson" "read_parquet" - [73] "reduce" "rolling_corr" - [75] "rolling_cov" "same_outer_dt" - [77] "scan_csv" "scan_ipc" - [79] "scan_ndjson" "scan_parquet" - [81] "select" "set_global_rpool_cap" - [83] "show_all_public_functions" "show_all_public_methods" - [85] "std" "struct" - [87] "sum" "sum_horizontal" - [89] "tail" "thread_pool_size" - [91] "threadpool_size" "using_string_cache" - [93] "var" "when" - [95] "with_string_cache" + [43] "date" "date_range" + [45] "datetime" "disable_string_cache" + [47] "dtypes" "duration" + [49] "element" "enable_string_cache" + [51] "expr_to_r" "first" + [53] "fold" "from_epoch" + [55] "get_global_rpool_cap" "head" + [57] "implode" "is_schema" + [59] "last" "len" + [61] "lit" "max" + [63] "max_horizontal" "mean" + [65] "median" "mem_address" + [67] "min" "min_horizontal" + [69] "n_unique" "numeric_dtypes" + [71] "raw_list" "read_csv" + [73] "read_ndjson" "read_parquet" + [75] "reduce" "rolling_corr" + [77] "rolling_cov" "same_outer_dt" + [79] "scan_csv" "scan_ipc" + [81] "scan_ndjson" "scan_parquet" + [83] "select" "set_global_rpool_cap" + [85] "show_all_public_functions" "show_all_public_methods" + [87] "std" "struct" + [89] "sum" "sum_horizontal" + [91] "tail" "thread_pool_size" + [93] "threadpool_size" "time" + [95] "using_string_cache" "var" + [97] "when" "with_string_cache" --- diff --git a/tests/testthat/test-lazy_functions.R b/tests/testthat/test-lazy_functions.R index d6a0427d2..5b74f822e 100644 --- a/tests/testthat/test-lazy_functions.R +++ b/tests/testthat/test-lazy_functions.R @@ -301,3 +301,105 @@ test_that("pl$from_epoch() errors if wrong time unit", { "one of" ) }) + +test_that("pl$datetime() works", { + df = pl$DataFrame( + year = 2019:2020, + month = 9:10, + day = 10:11, + min = 55:56 + ) + + expect_identical( + df$select( + dt_from_cols = pl$datetime("year", "month", "day", minute = "min", time_zone = "UTC"), + dt_from_lit = pl$datetime(2020, 3, 5, hour = 20:21, time_zone = "UTC"), + dt_from_mix = pl$datetime("year", 3, 5, second = 1, time_zone = "UTC") + )$to_list(), + list( + dt_from_cols = ISOdatetime(2019:2020, 9:10, 10:11, min = 55:56, 0, 0, tz = "UTC"), + dt_from_lit = ISOdatetime(2020, 3, 5, hour = 20:21, 0, 0, tz = "UTC"), + dt_from_mix = ISOdatetime(2019:2020, 3, 5, sec = 1, 0, 0, tz = "UTC") + ) + ) + + # floats are coerced to integers + expect_identical( + df$select(dt_floats = pl$datetime(2018.8, 5.3, 1, second = 2.1))$to_list(), + df$select(dt_floats = pl$datetime(2018, 5, 1, second = 2))$to_list() + ) + + # if datetime can't be constructed, it returns null + expect_identical( + df$select(dt_floats = pl$datetime(pl$lit("abc"), -2, 1))$to_list(), + list(dt_floats = as.POSIXct(NA)) + ) + + # can control the time_unit + # TODO: how can I test that? + expect_identical( + df$select( + dt_from_cols = pl$datetime("year", "month", "day", minute = "min", time_unit = "ms", time_zone = "UTC") + )$to_list(), + list( + dt_from_cols = ISOdatetime(2019:2020, 9:10, 10:11, min = 55:56, 0, 0, tz = "UTC") + ) + ) +}) + +test_that("pl$date() works", { + df = pl$DataFrame( + year = 2019:2020, + month = 9:10, + day = 10:11 + ) + + expect_identical( + df$select( + dt_from_cols = pl$date("year", "month", "day"), + dt_from_lit = pl$date(2020, 3, 5), + dt_from_mix = pl$date("year", 3, 5) + )$to_list(), + list( + dt_from_cols = as.Date(c("2019-09-10", "2020-10-11")), + dt_from_lit = as.Date(c("2020-3-5", "2020-3-5")), + dt_from_mix = as.Date(c("2019-3-5", "2020-3-5")) + ) + ) + + # floats are coerced to integers + expect_identical( + df$select(dt_floats = pl$date(2018.8, 5.3, 1))$to_list(), + df$select(dt_floats = pl$date(2018, 5, 1))$to_list() + ) + + # if datetime can't be constructed, it returns null + expect_identical( + df$select(dt_floats = pl$date(pl$lit("abc"), -2, 1))$to_list(), + list(dt_floats = as.Date(NA)) + ) +}) + +# TODO: I don't know if we can have an object with just "time" (without date) in +# base R +# test_that("pl$time() works", { +# df = pl$DataFrame(hour = 19:21, min = 9:11, sec = 10:12, micro = 1) +# +# expect_identical( +# df$select( +# time_from_cols = pl$time("hour", "min", "sec", "micro"), +# time_from_lit = pl$time(12, 3, 5), +# time_from_mix = pl$time("hour", 3, 5) +# )$to_list() +# ) +# +# # floats are coerced to integers +# df$select( +# time_floats = pl$time(12.5, 5.3, 1) +# ) +# +# # if time can't be constructed, it returns null +# df$select( +# time_floats = pl$time(pl$lit("abc"), -2, 1) +# ) +# }) diff --git a/tools/lib-sums.tsv b/tools/lib-sums.tsv deleted file mode 100644 index d2b965c37..000000000 --- a/tools/lib-sums.tsv +++ /dev/null @@ -1,6 +0,0 @@ -url sha256sum -https://github.com/pola-rs/r-polars/releases/download/lib-v0.38.1/libr_polars-0.38.1-aarch64-apple-darwin.tar.gz 74496332ba599d9829f8f418c8f1cfd1d445282c6173b661a6a8afb86b617498 -https://github.com/pola-rs/r-polars/releases/download/lib-v0.38.1/libr_polars-0.38.1-aarch64-unknown-linux-gnu.tar.gz 298ca83bf0b27883ec7c10d23dbfbe8e044032110a8b0a4f144ae55ab160f371 -https://github.com/pola-rs/r-polars/releases/download/lib-v0.38.1/libr_polars-0.38.1-x86_64-apple-darwin.tar.gz 2b8ad42c90dbc281149afae03e874ec47ccc8fcfa3ee064e7a364aa296bb1132 -https://github.com/pola-rs/r-polars/releases/download/lib-v0.38.1/libr_polars-0.38.1-x86_64-pc-windows-gnu.tar.gz 88a72fd6ace7574b30c2dd542ded040c7a291130048772ee4bdf006daef8b7a4 -https://github.com/pola-rs/r-polars/releases/download/lib-v0.38.1/libr_polars-0.38.1-x86_64-unknown-linux-gnu.tar.gz 8ba938024659256b9478af4b65ad9f7568b73581b3e9c99eb8edabb8159f5e27