Data model for birthdates.

Basic class to represent a birthdate, with rudimentary validation and
unit tests.

Change-Id: I56bcc0b8334f00fe36dcae05815ebd28f5fe2694
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3503904
Reviewed-by: Matthias Körber <koerber@google.com>
Reviewed-by: Christoph Schwering <schwering@google.com>
Commit-Queue: Florian Leimgruber <fleimgruber@google.com>
Cr-Commit-Position: refs/heads/main@{#981077}
diff --git a/components/autofill/core/browser/BUILD.gn b/components/autofill/core/browser/BUILD.gn
index 59be3ca..be20f26 100644
--- a/components/autofill/core/browser/BUILD.gn
+++ b/components/autofill/core/browser/BUILD.gn
@@ -117,6 +117,8 @@
     "data_model/autofill_structured_address_utils.h",
     "data_model/autofillable_data.cc",
     "data_model/autofillable_data.h",
+    "data_model/birthdate.cc",
+    "data_model/birthdate.h",
     "data_model/borrowed_transliterator.cc",
     "data_model/borrowed_transliterator.h",
     "data_model/contact_info.cc",
@@ -742,6 +744,7 @@
     "data_model/autofill_structured_address_test_utils.h",
     "data_model/autofill_structured_address_unittest.cc",
     "data_model/autofill_structured_address_utils_unittest.cc",
+    "data_model/birthdate_unittest.cc",
     "data_model/borrowed_transliterator_unittest.cc",
     "data_model/contact_info_unittest.cc",
     "data_model/credit_card_unittest.cc",
diff --git a/components/autofill/core/browser/data_model/birthdate.cc b/components/autofill/core/browser/data_model/birthdate.cc
new file mode 100644
index 0000000..bb49400
--- /dev/null
+++ b/components/autofill/core/browser/data_model/birthdate.cc
@@ -0,0 +1,74 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/autofill/core/browser/data_model/birthdate.h"
+
+#include "base/notreached.h"
+#include "base/strings/string_number_conversions.h"
+#include "components/autofill/core/browser/autofill_type.h"
+
+namespace autofill {
+
+using structured_address::VerificationStatus;
+
+bool operator==(const Birthdate& a, const Birthdate& b) {
+  return a.day_ == b.day_ && a.month_ == b.month_ && a.year_ == b.year_;
+}
+
+std::u16string Birthdate::GetRawInfo(ServerFieldType type) const {
+  DCHECK_EQ(AutofillType(type).group(), FieldTypeGroup::kBirthdateField);
+
+  auto ToStringOrEmpty = [](int n) {
+    return n != 0 ? base::NumberToString16(n) : std::u16string();
+  };
+
+  switch (type) {
+    case BIRTHDATE_DAY:
+      return ToStringOrEmpty(day_);
+    case BIRTHDATE_MONTH:
+      return ToStringOrEmpty(month_);
+    case BIRTHDATE_YEAR_4_DIGITS:
+      return ToStringOrEmpty(year_);
+    default:
+      return std::u16string();
+  }
+}
+
+void Birthdate::SetRawInfoWithVerificationStatus(ServerFieldType type,
+                                                 const std::u16string& value,
+                                                 VerificationStatus status) {
+  DCHECK_EQ(AutofillType(type).group(), FieldTypeGroup::kBirthdateField);
+
+  // If |value| is not a number, |StringToInt()| sets it to 0, which will clear
+  // the field.
+  int int_value;
+  base::StringToInt(value, &int_value);
+
+  auto ValueIfInRangeOrZero = [int_value](int lower_bound, int upper_bound) {
+    return lower_bound <= int_value && int_value <= upper_bound ? int_value : 0;
+  };
+  // Set the appropriate field to |int_value| if it passes some rudimentary
+  // validation. Otherwise clear it.
+  switch (type) {
+    case BIRTHDATE_DAY:
+      day_ = ValueIfInRangeOrZero(1, 31);
+      break;
+    case BIRTHDATE_MONTH:
+      month_ = ValueIfInRangeOrZero(1, 12);
+      break;
+    case BIRTHDATE_YEAR_4_DIGITS:
+      year_ = ValueIfInRangeOrZero(1900, 9999);
+      break;
+    default:
+      NOTREACHED();
+  }
+}
+
+void Birthdate::GetSupportedTypes(ServerFieldTypeSet* supported_types) const {
+  supported_types->insert(BIRTHDATE_DAY);
+  supported_types->insert(BIRTHDATE_MONTH);
+  supported_types->insert(BIRTHDATE_YEAR_4_DIGITS);
+}
+
+}  // namespace autofill
diff --git a/components/autofill/core/browser/data_model/birthdate.h b/components/autofill/core/browser/data_model/birthdate.h
new file mode 100644
index 0000000..24184b3
--- /dev/null
+++ b/components/autofill/core/browser/data_model/birthdate.h
@@ -0,0 +1,47 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_AUTOFILL_CORE_BROWSER_DATA_MODEL_BIRTHDATE_H_
+#define COMPONENTS_AUTOFILL_CORE_BROWSER_DATA_MODEL_BIRTHDATE_H_
+
+#include <string>
+
+#include "components/autofill/core/browser/data_model/form_group.h"
+
+namespace autofill {
+
+// A form group that stores birthdate information.
+class Birthdate : public FormGroup {
+ public:
+  Birthdate() = default;
+  Birthdate(const Birthdate& other) = default;
+  Birthdate& operator=(const Birthdate& other) = default;
+
+  friend bool operator==(const Birthdate& a, const Birthdate& b);
+  friend bool operator!=(const Birthdate& a, const Birthdate& b) {
+    return !(a == b);
+  }
+
+  // FormGroup:
+  std::u16string GetRawInfo(ServerFieldType type) const override;
+
+  void SetRawInfoWithVerificationStatus(
+      ServerFieldType type,
+      const std::u16string& value,
+      structured_address::VerificationStatus status) override;
+
+ private:
+  // FormGroup:
+  void GetSupportedTypes(ServerFieldTypeSet* supported_types) const override;
+
+  // Zero represents an unset value.
+  int day_ = 0;
+  int month_ = 0;
+  // 4 digits.
+  int year_ = 0;
+};
+
+}  // namespace autofill
+
+#endif  // COMPONENTS_AUTOFILL_CORE_BROWSER_DATA_MODEL_BIRTHDATE_H_
diff --git a/components/autofill/core/browser/data_model/birthdate_unittest.cc b/components/autofill/core/browser/data_model/birthdate_unittest.cc
new file mode 100644
index 0000000..6598bbf
--- /dev/null
+++ b/components/autofill/core/browser/data_model/birthdate_unittest.cc
@@ -0,0 +1,76 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/autofill/core/browser/data_model/birthdate.h"
+
+#include "components/autofill/core/browser/field_types.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace autofill {
+namespace {
+
+Birthdate CreateBirthdate(const std::u16string& day,
+                          const std::u16string& month,
+                          const std::u16string& year) {
+  Birthdate birthdate;
+  birthdate.SetRawInfo(BIRTHDATE_DAY, day);
+  birthdate.SetRawInfo(BIRTHDATE_MONTH, month);
+  birthdate.SetRawInfo(BIRTHDATE_YEAR_4_DIGITS, year);
+  return birthdate;
+}
+
+void VerifyValues(const Birthdate& birthdate,
+                  const std::u16string& day,
+                  const std::u16string& month,
+                  const std::u16string& year) {
+  EXPECT_EQ(birthdate.GetRawInfo(BIRTHDATE_DAY), day);
+  EXPECT_EQ(birthdate.GetRawInfo(BIRTHDATE_MONTH), month);
+  EXPECT_EQ(birthdate.GetRawInfo(BIRTHDATE_YEAR_4_DIGITS), year);
+}
+
+// Expect that setting |field| to |value| clears the |field|. This is used to
+// test invalid and empty values.
+void SetFieldAndExpectEmpty(ServerFieldType field,
+                            const std::u16string& value) {
+  Birthdate birthdate = CreateBirthdate(u"14", u"3", u"1997");
+  VerifyValues(birthdate, u"14", u"3", u"1997");
+  birthdate.SetRawInfo(field, value);
+  EXPECT_TRUE(birthdate.GetRawInfo(field).empty());
+}
+
+}  // anonymous namespace
+
+// Tests writing and reading various valid birthdates.
+TEST(BirthdateTest, ValidBirthdate) {
+  Birthdate birthdate = CreateBirthdate(u"14", u"3", u"1997");
+  VerifyValues(birthdate, u"14", u"3", u"1997");
+
+  // Leading zeros are fine, but they are trimmed internally.
+  birthdate = CreateBirthdate(u"07", u"06", u"1997");
+  VerifyValues(birthdate, u"7", u"6", u"1997");
+
+  // Leading and trailing spaces are fine too.
+  birthdate = CreateBirthdate(u"24 ", u"  12", u" 2022 ");
+  VerifyValues(birthdate, u"24", u"12", u"2022");
+}
+
+// Tests that invalid values clear the corresponding fields.
+TEST(BirthdateTest, Validation) {
+  SetFieldAndExpectEmpty(BIRTHDATE_DAY, u"42");
+  SetFieldAndExpectEmpty(BIRTHDATE_DAY, u"NaN");
+  SetFieldAndExpectEmpty(BIRTHDATE_MONTH, u"13");
+  SetFieldAndExpectEmpty(BIRTHDATE_MONTH, u"a");
+  SetFieldAndExpectEmpty(BIRTHDATE_YEAR_4_DIGITS, u"12345");
+  SetFieldAndExpectEmpty(BIRTHDATE_YEAR_4_DIGITS, u"1234");
+}
+
+// Tests that empty values clear the corresponding fields.
+TEST(BirthdateTest, Clear) {
+  for (const ServerFieldType component :
+       {BIRTHDATE_DAY, BIRTHDATE_MONTH, BIRTHDATE_YEAR_4_DIGITS}) {
+    SetFieldAndExpectEmpty(component, u"");
+  }
+}
+
+}  // namespace autofill