Chrome80+LEXの日時型入力で日付がずれる件の調査メモ

すでにIssueとして挙がっている

https://success.salesforce.com/issues_view?id=a1p3A000001Go5aQAC

ので~~修正待つだけだけど~~Spring'20で修正されたので修正点を一番下に追記しました。

ローカルに置いとくと邪魔なので調査時のメモを供養。

----

日時のハンドラっぽい関数にブレークポイントを設置しまくってハンドラを見つける

components/ui/inputDateTime.js > setDateTimeValue

this.dateTimeLib.dateTimeService.getISOValue

の生成がおかしい

libraries/ui/dateTimeLib/dateTimeService.js

```js
getISOValue:function(date, config, callback) {
    var hours = config.hours;
    var minutes = config.minutes;
    if(hours) {
      date = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), hours, minutes)
    }
    var isoValue = function isoValue(convertedDate) {
      var translatedDate = $A.localizationService.translateFromOtherCalendar(convertedDate);
      var isoString = translatedDate.toISOString();
      callback(isoString)
    };
    convertFromTimezone(date, config.timezone, $A.getCallback(isoValue))
  }, formatYear:function(year) {
    var jpYear = $A.get("$Locale.showJapaneseImperialYear") && getImperialYear(year);
    return year + (jpYear ? " (" + jpYear + ")" : "")
  }}
```

convertFromTimezone が変換をかけているのに、 isoValueの中でもう一度 translatedDate.toISOString で変換がかかっている

convertedDate が、Firefoxだともとの数値、Chrome80だと前日になっている。

isoValue 内にブレークポイントを設置してコールスタックを見ると、(以下はスタックコピー用に作ったError)

```
at eval (eval at isoValue (/libraries/ui/dateTimeLib/dateTimeService.js), <anonymous>:1:7)
at isoValue (/libraries/ui/dateTimeLib/dateTimeService.js:91:7)
at callbackWrapper (/auraFW/javascript/5fuxCiO1mNHGdvJphU5ELQ/aura_proddebug.js:46084:23)
at AuraLocalizationService.$WallTimeToUTC$ (/auraFW/javascript/5fuxCiO1mNHGdvJphU5ELQ/aura_proddebug.js:43984:3)
at convertFromTimezone (/libraries/ui/dateTimeLib/dateTimeService.js:11:28)
at Object.getISOValue (/libraries/ui/dateTimeLib/dateTimeService.js:93:5)
at Object.setDateTimeValue (/components/ui/inputDateTime.js:434:46)
at Object.setTimeValue (/components/ui/inputDateTime.js:412:18)
at Object.handleDateTimeSelection (/components/ui/inputDateTime.js:370:22)
at setDateTime (/components/ui/inputDateTime.js:51:16)
```

以下の関数の `data.offset` が違う。Firefox:540, Chrome:1980

```js
AuraLocalizationService.prototype.$WallTimeToUTC$ = function(date, timezone, callback) {
  $A.assert(date instanceof Date, "AuraLocalizationService.WallTimeToUTC(): 'date' must be a Date object.");
  $A.assert(typeof callback === "function", "AuraLocalizationService.WallTimeToUTC(): callback must be a function.");
  timezone = this.$normalizeTimeZone$(timezone);
  if(timezone === "UTC" || !this.$isValidDateObject$(date)) {
    callback(date);
    return
  }
  var data = this.$createDateTimeData$(date, timezone);
  var convertedData = this.$setDataToZone$(data, "UTC");
  var dateTime = convertedData["config"];
  var ts = Date.UTC(dateTime["year"], dateTime["month"] - 1, dateTime["day"], dateTime["hour"], dateTime["minute"]);
  var utcDate = new Date(ts);
  utcDate.setUTCSeconds(date.getSeconds(), date.getMilliseconds());
  callback(utcDate)
};
```

1980 - 540 = 1440 = 1日あたりの分。

data を生成している this.$createDateTimeData$ を参照。

$A.localizationService.$createDateTimeData$

```js
AuraLocalizationService.prototype.$createDateTimeData$ = function(date, timeZone) {
  var config = {"year":date.getUTCFullYear(), "month":date.getUTCMonth() + 1, "day":date.getUTCDate(), "hour":date.getUTCHours(), "minute":date.getUTCMinutes()};
  var zoneInfo = this.$getZoneInfo$(config, timeZone);
  return{"config":config, "offset":zoneInfo[1], "timestamp":zoneInfo[0], "timeZone":timeZone}
};
```

this.$getZoneInfo$(config, timeZone);
zoneInfo[1] が1980。

AuraLocalizationService.prototype.$getZoneInfo$ を参照

```js
AuraLocalizationService.prototype.$getZoneInfo$ = function(config, timeZone) {
  var nowOffset = this.$zoneOffset$(Date.now(), timeZone);
  var localTs = Date.UTC(config["year"], config["month"] - 1, config["day"], config["hour"], config["minute"]);
  var utcGuess = localTs - nowOffset * 6E4;
  var guessOffset = this.$zoneOffset$(utcGuess, timeZone);
  if(nowOffset === guessOffset) {
    return[utcGuess, guessOffset]
  }
  utcGuess -= (guessOffset - nowOffset) * 6E4;
  var guessOffset2 = this.$zoneOffset$(utcGuess, timeZone);
  if(guessOffset === guessOffset2) {
    return[utcGuess, guessOffset]
  }
  return[localTs - Math.max(guessOffset, guessOffset2) * 6E4, Math.max(guessOffset, guessOffset2)]
};
```

localTs はChromeとFirefoxで同じ値。
utcGuess も。
guessOffset からずれている。

```js
// timestamp = 1581433200000 で調査
AuraLocalizationService.prototype.$zoneOffset$ = function(timestamp, timeZone) {
  if(timeZone === "UTC") {
    return 0
  }
  var date = new Date(timestamp);
  date.setSeconds(0, 0);
  var dateTimeString = this.$formatDateToEnUSString$(date, timeZone);
  var zoneTs = this.$parseEnUSDateTimeString$(dateTimeString);
  return(zoneTs - date.getTime()) / 6E4
};
```
dateTimeString が Chrome:"02/12/2020, 24:00" Firefox:"02/12/2020, 00:00"

Chromeに24時が存在する。

```js
AuraLocalizationService.prototype.$formatDateToEnUSString$ = function(date, timeZone) {
  var timeZoneFormat = this.$createEnUSDateTimeFormat$(timeZone);
  var dateString = timeZoneFormat ? this.$format$(timeZoneFormat, date) : null;
  return dateString !== null ? dateString : this.$formatDateTime$(date, "MM/dd/yyyy, hh:mm")
};
```

timeZoneFormat.format(new Date(2020, 2, 1, 0, 0))
//=> "03/01/2020, 24:00"
timeZoneFormat.format
//=> ƒ () { [native code] }

```js
AuraLocalizationService.prototype.$createEnUSDateTimeFormat$ = function(timeZone) {
  var timeZoneFormat = this.$timeZoneFormatCache$[timeZone];
  if(timeZoneFormat !== undefined) {
    return timeZoneFormat
  }
  try {
    timeZoneFormat = Intl["DateTimeFormat"]("en-US", {"timeZone":timeZone, "hour12":false, "year":"numeric", "month":"2-digit", "day":"2-digit", "hour":"2-digit", "minute":"2-digit"})
  }catch(e) {
    timeZoneFormat = null
  }
  this.$timeZoneFormatCache$[timeZone] = timeZoneFormat;
  return timeZoneFormat
};
```

f = Intl.DateTimeFormat("en-US", {timeZone:"Asia/Tokyo", hour12: false, year:"numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit"});
f.format(new Date(2020, 3, 1, 0, 0));
//=> Chrome: "04/01/2020, 24:00"
//=> Firefox: "04/01/2020, 00:00"

f = Intl.DateTimeFormat("en-US", {timeZone:"Asia/Tokyo", hour12: false, year:"numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit"});
f.format(new Date(2020, 3, 1, 0, 59));
//=> "04/01/2020, 24:59"

f = Intl.DateTimeFormat("en-US", {timeZone:"Asia/Tokyo", hour12: false, year:"numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit"});
f.format(new Date(2020, 3, 1, 0, 60));
//=> "04/01/2020, 01:00"

----

後続のパース処理 parseEnUSDateTimeString の調査

```js
AuraLocalizationService.prototype.$parseEnUSDateTimeString$ = function(dateTimeString) {
  var match = this.$enUsDateTimePatterns$.$primaryPattern$.$REG_EXP$.exec(dateTimeString);
  if(match === null) {
    match = this.$enUsDateTimePatterns$.$secondaryPattern$.$REG_EXP$.exec(dateTimeString);
    if(match === null) {
      return null
    }
    this.$enUsDateTimePatterns$.$swap$()
  }
  return Date.UTC(parseInt(match[this.$enUsDateTimePatterns$.$primaryPattern$.$YEAR$], 10), parseInt(match[this.$enUsDateTimePatterns$.$primaryPattern$.$MONTH$], 10) - 1, parseInt(match[this.$enUsDateTimePatterns$.$primaryPattern$.$DAY$], 10), parseInt(match[this.$enUsDateTimePatterns$.$primaryPattern$.$HOUR$], 10), parseInt(match[this.$enUsDateTimePatterns$.$primaryPattern$.$MINUTE$], 10))
};
```

2020/02/13 00:30 の時、 dateTimeStringは 「02/13/2020, 24:30」 となり、
match
//=> ["02/13/2020, 24:30", "02", "13", "2020", "24", "30", index: 0, input: "02/13/2020, 24:30", groups: undefined]

なのでここで最後の Date.UTC 引数の時間指定も 24 になり、次の日のインスタンスが生成される。

`AuraLocalizationService.prototype.$zoneOffset$` の最後が

```js
  return(zoneTs - date.getTime()) / 6E4
```

で、この次の日で生成された時間と、指定されたタイムスタンプの差分をオフセットとして返している。

----

Spring'20とともに修正されました。

```js
AuraLocalizationService.prototype.$createEnUSDateTimeFormat$ = function(timeZone) {
  var timeZoneFormat = this.$timeZoneFormatCache$[timeZone];
  if(timeZoneFormat !== undefined) {
    return timeZoneFormat
  }
  try {
    var testDateTime = Intl["DateTimeFormat"]("en-US", {"hour12":false, "timeZone":"America/New_York", "year":"numeric", "month":"2-digit", "day":"2-digit", "hour":"2-digit", "minute":"2-digit", "second":"2-digit"});
    var testDateTimeFormatted = testDateTime["format"](new Date("2014-06-25T04:00:00.123Z"));
    if(testDateTimeFormatted === "06/25/2014, 00:00:00" || testDateTimeFormatted === "\u200e06\u200e/\u200e25\u200e/\u200e2014\u200e \u200e00\u200e:\u200e00\u200e:\u200e00") {
      timeZoneFormat = Intl["DateTimeFormat"]("en-US", {"hour12":false, "timeZone":timeZone, "year":"numeric", "month":"2-digit", "day":"2-digit", "hour":"2-digit", "minute":"2-digit", "second":"2-digit"})
    }else {
      timeZoneFormat = Intl["DateTimeFormat"]("en-US", {"hourCycle":"h23", "timeZone":timeZone, "year":"numeric", "month":"2-digit", "day":"2-digit", "hour":"2-digit", "minute":"2-digit", "second":"2-digit"})
    }
  }catch(e) {
    timeZoneFormat = null
  }
  this.$timeZoneFormatCache$[timeZone] = timeZoneFormat;
  return timeZoneFormat
};
```

固定の日時でフォーマットした結果が0時表記と一致しなければhourCycleにh23を指定するように、という修正です。

他にもやり方はあるでしょうが、影響を最小限に留めるためにこの修正を選択したのでしょう。