Google Earth Engine – Convert Cumulative Day of Year to Milliseconds

google-earth-enginegoogle-earth-engine-javascript-api

I'm working with an image where the value of each pixel corresponds to the day of the year, starting with 2019-01-01.

Date Pixel value
2019-01-01 1
2019-01-02 2
2019-01-03 3
2022-04-11 1196

I want to convert the value of these pixels (cumulative day of year) into milliseconds, but I couldn't think of a way to do it.

Unfortunately I can't provide an image to reproduce the code.

Best Answer

Since 1196 should give the millis for 2022-04-11, it seems like you want to exclude leap days altogether. Is this the case?

If you exclude leap days, it's quite simple:

function toMillis(day) {
  var dayOffset = ee.Date('2019-01-01')
    .difference(ee.Date('1970-01-01'), 'days')
    .subtract(1)
  return day.add(dayOffset)
    .multiply(1000) // Second to millis
    .multiply(3600) // Hour to second
    .multiply(24) // Day to hour
}

Otherwise, it's tricky. Since you need to do date manipulations on an image, you cannot rely on ee.Date, so you have to manually deal with the date logic, which is notoriously easy to get wrong. Anyway, below is my stab at implementing something like this. I'm sure there are problems with the implementation, it certainly it has some assumptions that causes this to fail if your date is too far in the future (2070s).

function toMillis(day) {
  var referenceYear = ee.Number(2019)
  // Could be different due to leap years. 
  var approximateYear = day.subtract(1).divide(365).floor().add(referenceYear)

  var leapYears = ee.Image(ee.Array( // Array image with leap years 
    ee.List.sequence(referenceYear, 2070)
      .map(function isLeapYear(year) {
        year = ee.Number(year)
        return ee.Date.fromYMD(year.add(1), 1, 1).advance(-1, 'day') // Last of year
          .getRelative('day', 'year').gt(364) // Got the extra day
          .multiply(year) // Return the actual year if a leap year or 0
      })
      .filter(ee.Filter.neq('item', 0))
  ))
  var numberOfLeapYears = leapYears
    .arrayMask(leapYears.lte(approximateYear)) // Mask out leap years after the year of the pixel
    .arrayLength(0) // Results in an image where the pixel value represents the number of leap years

  var leapYearAdjustedDay = day.add(numberOfLeapYears)
  var year = leapYearAdjustedDay.subtract(1).divide(365).floor().add(referenceYear)
  
  var isLeapYear = leapYears.arrayMask(leapYears.eq(year))
    .arrayLength(0)
  var januaryFirstDay = year.subtract(referenceYear).multiply(365).add(numberOfLeapYears).add(1).subtract(isLeapYear)
  var dayOfYear = leapYearAdjustedDay.subtract(januaryFirstDay).add(1)
  var isBeforeLeapDay = dayOfYear.lte(31 + 29)
  var timeOfYearAdjustment = isLeapYear.and(isBeforeLeapDay) // 1 if before leap day on leap year, otherwise 0
  var finalDay = leapYearAdjustedDay
    .subtract(timeOfYearAdjustment)
  
  var dayOffset = ee.Date.fromYMD(referenceYear, 1, 1)
    .difference(ee.Date('1970-01-01'), 'days')
    .subtract(1)
  return finalDay.add(dayOffset)
    .multiply(1000) // Second to millis
    .multiply(3600) // Hour to second
    .multiply(24) // Day to hour
}


testDate(toMillis(ee.Image(1)), '2019-01-01')
testDate(toMillis(ee.Image(2)), '2019-01-02')
testDate(toMillis(ee.Image(3)), '2019-01-03')
testDate(toMillis(ee.Image(365)), '2019-12-31')
testDate(toMillis(ee.Image(366)), '2020-01-01')
testDate(toMillis(ee.Image(423)), '2020-02-27')
testDate(toMillis(ee.Image(424)), '2020-02-28')
testDate(toMillis(ee.Image(425)), '2020-03-01')
testDate(toMillis(ee.Image(426)), '2020-03-02')
testDate(toMillis(ee.Image(730)), '2020-12-31')
testDate(toMillis(ee.Image(731)), '2021-01-01')
testDate(toMillis(ee.Image(732)), '2021-01-02')
testDate(toMillis(ee.Image(1196)), '2022-04-11')


function testDate(millisImage, expectedDate) {
  var millis = millisImage
    .reduceRegion(ee.Reducer.first(), ee.Geometry.Point([0, 0]), 1)
    .values()
    .getNumber(0)
  var date = ee.Date(millis).format('yyyy-MM-dd')
  var valid = millis.eq(ee.Date(expectedDate).millis())
  print(
    ee.Algorithms.If(valid,
      'Success: ' + expectedDate,
      ee.String('Failed: Expected ' + expectedDate + ' was ').cat(date)
    )
  )
}

https://code.earthengine.google.com/16a23b3554e90c869c999de09e2fde90