ionic-team / ionic-framework

A powerful cross-platform UI toolkit for building native-quality iOS, Android, and Progressive Web Apps with HTML, CSS, and JavaScript.
https://ionicframework.com
MIT License
51.09k stars 13.51k forks source link

ion-datetime returns incorrect UTC datetime #11408

Closed tommck closed 5 years ago

tommck commented 7 years ago

Ionic version: (check one with "x") [ ] 1.x [x] 2.x [x] 3.x

I'm submitting a ... (check one with "x") [x] bug report [ ] feature request [ ] support request => Please do not submit support requests here, use one of these channels: https://forum.ionicframework.com/ or http://ionicworldwide.herokuapp.com/

Current behavior: The ion-datetime returns an ISO 8601 string including the Time Zone, (e.g. "2017-01-01T13:00:00Z" when I enter 1:00pm in the Eastern time zone)

Expected behavior: I would expect it to return "2017-01-01T13:00:00" with no time zone or "2017-01-01T13:00:00-05:00", which is the correct time offset for my location.

The value from the ion-datetime should either be local timezone aware, or not add timezone information into the date string. As it is now, it is just wrong. Since it has "Z" on the end, it is forcing the result to be UTC/Zulu time. Thus, even using a library like momentjs won't allow you to easily convert the value.

Steps to reproduce: just enter a date into an ion-date and look at the output value.

Ionic info: (run ionic info from a terminal/cmd prompt and paste output below): Your system information:

ordova CLI: 6.5.0 Ionic Framework Version: 3.0.1 Ionic CLI Version: 2.2.2 Ionic App Lib Version: 2.2.1 Ionic App Scripts Version: 1.3.4 ios-deploy version: Not installed ios-sim version: Not installed OS: Windows 10 Node Version: v6.10.0 Xcode version: Not installed

Here's a plunkr for kicks: http://plnkr.co/edit/eNxoq89PlhUxWEiX6uen?p=preview

tommck commented 7 years ago

Now, I'm aware of this: https://github.com/driftyco/ionic/issues/8009

But the problem is that the datetime value returned is actually WRONG. This isn't just an "it's hard to do this in a cross platform way" kind of issue. If you can't do it in a cross platform way, that's fine, but at least don't return incorrect data.

hitpopdimestop commented 7 years ago

Have different issue with ion-datetime picker in ionic 3.1.0, I described it on ionic forum. Feels like these changes should be better documented

jgw96 commented 7 years ago

Hello, all! We are looking into this. Unfortunately, it looks like this is a breaking change that slipped through.

jgw96 commented 7 years ago

Hey, @hitpopdimestop would you mind posting the code you're using?

hitpopdimestop commented 7 years ago

Sorry for late response, had to reproduce it once again in a separate branch. I cannot post the entire code, but this is what you're interested about. Part of the template:

<ion-datetime displayFormat="D MMM"
                        pickerFormat="DD MMMM YYYY"
                        min="{{today}}"
                        max="2020"
                        [(ngModel)]="checkInDate"
                        (ionChange)="changeCheckOutDate()"
                        #checkInInput>
</ion-datetime>
<ion-datetime displayFormat="D MMM"
                        pickerFormat="DD MMMM YYYY"
                        [min]="minCheckOutDate()"
                        max="2020"
                        [(ngModel)]="checkOutDate"
                        #checkOutInput>
</ion-datetime>

And part of my TS:

  @ViewChild('checkInInput') checkInInput;
  @ViewChild('checkOutInput') checkOutInput;
  checkInDate: any;
  checkOutDate: any;
  today: string;

  constructor(...) {
    this.today = moment().format('YYYY-MM-DD');
    this.checkInDate  = moment().format('YYYY-MM-DD');
    this.checkOutDate = moment().add(1, 'days').format('YYYY-MM-DD');
}

  changeCheckOutDate() {
    if(moment(this.checkInDate).isSameOrAfter(this.checkOutDate)) {
      this.checkOutDate = moment(this.checkInDate).add(1, 'day').format('YYYY-MM-DD');
    }
  }

  minCheckOutDate() {
    return moment(this.checkInDate).add(1, 'day').format('YYYY-MM-DD');
  }

Here is what I get after submitting the form if I don't change anything in the input:

CheckInDate 2017-04-27 CheckOutDate 2017-04-28

And if I change only CheckIn for April, 30th:

CheckInDate {"year":2017,"month":4,"day":30,"hour":null,"minute":null,"second":null,"millisecond":null,"tzOffset":0} CheckOutDate 2017-05-31

If you need any additional info, then just ask me. And thank you for paying attention. Dmytro

tommck commented 7 years ago

@jgw96 - You're saying my issue is an accidental breaking change? I think my issue's been hijacked

hitpopdimestop commented 7 years ago

@tommck - I thought that collecting all the issues on the same component was a good idea. By the way, has 3.1.1 addressed your issue? I will check mine after we finish deploy in few days.

cyberbobjr commented 7 years ago

I confirm, with 3.1.1 the datetime component works well (with ISO String)

tommck commented 7 years ago

With the latest of everything, the control is still returning a full ISO string "2017-01-01T13:00:00Z"

It's assuming UTC when it's not timezone aware.. so this is wrong. Same issue still exists

tommck commented 7 years ago

@hitpopdimestop each issue should separate. reporting more than 1 problem in one issue just gets confusing.

tommck commented 7 years ago

@jgw96 please read the original issue and not the comments, since they're not about my issue

JustinPierce commented 7 years ago

@jgw96 I can confirm @tommck's issue. The behavior is absolutely incorrect. In order to reproduce it:

  1. Set an <ion-datetime>'s value to a date/time string in the format YYYY-MM-DDTHH:mm:ss.
  2. Observe that the component's value does not end with a Z. This is interpreted as local time.
  3. Change the component's value using the UI.
  4. Observe that the component's value is the time displayed on the control, only with a Z appended. This is almost certainly not the same as your local time.

The relevant issue is that the component is taking a timezone-naive value and making it timezone-aware without adjusting the time to compensate for the change. Timezones are already really, really hard. The component is just making them even harder.

Demo Plunkr: http://plnkr.co/edit/4OHGcOK2XIjHgWdHP54i?p=preview

To trigger the bug, just open the datetime picker and then click "Done". You don't even need to change the date.

Simpler1 commented 7 years ago

I'm having the same problem as @tommck. Any update on this?

peng-jiesi commented 7 years ago

same error

When component update val will invoke _inputNormalize I see parseDate (in datetime-util.ts/) set tzOffset is 0, but i want it's a local value.

Simpler1 commented 7 years ago

Here are some helper methods to get past this shortcoming of the <ion-datetime> element.

  /* Convert a real ISO 8601 UTC date stringto a BOGUS ISO 8601 local date string (with a Z).
   * utcDateString should be of format:  YYYY-MM-DDTHH-mm-ss.sssZ
   */
  convertISO8601UTCtoLocalwZ(utcDateString: string): string {
    const ISO_8601_UTC_REGEXP = /^(\d{4})(-\d{2})(-\d{2})T(\d{2})(\:\d{2}(\:\d{2}(\.\d{3})?)?)?Z$/;
    try {
      if (utcDateString.match(ISO_8601_UTC_REGEXP)) {
        let localDateString: string;
        let utcDate: Date = new Date(utcDateString);
        let tzOffset: number = new Date().getTimezoneOffset() * 60 * 1000;
        let newTime: number = utcDate.getTime() - tzOffset;
        let localDate: Date = new Date(newTime);
        localDateString = localDate.toJSON()
        return localDateString;
      } else {
        throw 'Incorrect UTC ISO8601 date string';
      }
    }
    catch(err) {
      alert('Date string is formatted incorrectly: \n' + err);
    }
  }

  /* Convert a BOGUS ISO 8601 Local date string (with a Z) to a real ISO 8601 UTC date string.
   * localDateString should be of format:  YYYY-MM-DDTHH-mm-ss.sssZ
   */
  convertISO8601LocalwZtoUTC(localDateString: string): string {
    const ISO_8601_UTC_REGEXP = /^(\d{4})(-\d{2})(-\d{2})T(\d{2})(\:\d{2}(\:\d{2}(\.\d{3})?)?)?Z$/;
    try {
      if (localDateString.match(ISO_8601_UTC_REGEXP)) {
        let utcDateString: string;
        let localDate: Date = new Date(localDateString);
        let tzOffset: number = new Date().getTimezoneOffset() * 60 * 1000;
        let newTime: number = localDate.getTime() + tzOffset;
        let utcDate: Date = new Date(newTime);
        utcDateString = utcDate.toJSON()
        return utcDateString;
      } else {
        throw 'Incorrect BOGUS local ISO8601 date string';
      }
    }
    catch(err) {
      alert('Date string is formatted incorrectly: \n' + err);
    }
  }

  /* Convert an ISO 8601 Local date string (no Z) to a BOGUS ISO 8601 UTC date string.
   * localDateString should be of format:  YYYY-MM-DDTHH-mm-ss.sss
   */
  appendZ(localDateString: string): string {
    const ISO_8601_LOCAL_REGEXP = /^(\d{4})(-\d{2})(-\d{2})T(\d{2})(\:\d{2}(\:\d{2}(\.\d{3})?)?)?$/;
    try {
      if (localDateString.match(ISO_8601_LOCAL_REGEXP)) {
        return localDateString + "Z";
      } else {
        throw 'Incorrect local ISO8601 date string';
      }
    }
    catch(err) {
      alert('Date string is formatted incorrectly: \n' + err);
    }
  }

  /* Convert a BOGUS ISO 8601 Local date string (with a Z) to a real ISO 8601 local date string.
   * bogusLocalDateString should be of format:  YYYY-MM-DDTHH-mm-ss.sssZ
   */
  removeZ(bogusLocalDateString: string): string {
    const ISO_8601_UTC_REGEXP = /^(\d{4})(-\d{2})(-\d{2})T(\d{2})(\:\d{2}(\:\d{2}(\.\d{3})?)?)?Z$/;
    try {
      if (bogusLocalDateString.match(ISO_8601_UTC_REGEXP)) {
        return bogusLocalDateString.slice(0,-1);
      } else {
        throw 'Incorrect BOGUS local ISO8601 date string';
      }
    }
    catch(err) {
      alert('Date string is formatted incorrectly: \n' + err);
    }
  }

The first two are for when you're storing your dates as valid ISO 8601 UTC date strings and need to convert to a bogus string in order to display as local time in your form.

The second two are for when you're storing you dates as a valid ISO 8601 local date strings and need to convert to a bogus string in order to display as local time in your form.

joewoodhouse commented 7 years ago

Any update on this issue?

barakbd commented 7 years ago

We have also encountered this issue. Any ionic solution in the horizon?

Simpler1 commented 7 years ago

As a workaround, you can use moment.js to convert your ISO8601 strings 2017-10-10T13:00:00Z to include the timezone offset 2017-10-10T08:00:00-05:00 before using in the ion-datetime: moment(utcDateString).format(); // 2017-10-10T13:00:00Z -> 2017-10-10T08:00:00-05:00 and then convert it back before storing the value (if you want to store as UTC): moment(localDateString).toISOString(); // 2017-10-10T08:00:00-05:00 -> 2017-10-10T13:00:00Z

ion-datetime currently only uses the first 10 characters of the string and ignores any "Z" or timezone offset, but when the value is modified, it will replace the "Z" or the timezone offset, but if neither were there originally (representing a local time), then a "Z" is incorrectly added.

danidelgadoz commented 6 years ago

use moment as ionic recomends on its documentation https://ionicframework.com/docs/api/components/datetime/DateTime/#advanced-datetime-validation-and-manipulation

  1. Open console at root proyect and install moment: npm install moment --S.
    1. Import moment in component file: import moment from 'moment';.
    2. Set value of model variable: this.myDate = moment().format().
JustinPierce commented 6 years ago

@dedd1993 Simply using moment will not save you. Even if you use moment, ion-datetime will still screw up your time zones unless you take precautions such as those described by @Simpler1.

inejose commented 6 years ago

Hi,

I'm dealing with the same problem. I work and store dates in a "YYYY-MM-DDTHH:mm" format. When I use ion-datetime, a end Z is added, and when I try to parse or manipulate with Date() or moment(), hours are +1 hour than selected. I'm trying everything without success.

Previously, I initializate the date variable as indicated in the documentation, but then, the string is converted with a Z.

image

startTimeForm: string;
startTimeForm = moment("2018-01-01T00:00").format("YYYY-MM-DDTHH:mm");
//startTimeForm = "2018-01-01T00:00"
//From view, If I select a new time like 15:00, the string is converted to "2018-01-01T15:00Z" 
//Now, If I try to convert  "2018-01-01T15:00Z" to get the HH o mm values, with moment() or Date(), time is 16:00, not 15:00

Please, help, which approach would be better for have a consistency between selected Time and String in that format (without Z or timezone changes). I have spent many hours with this.

Thanks in advance.

inejose commented 6 years ago

SOLVED. Finally, I initializated correctly the time variables and everything suddenly works :)

Instead of:

startTimeForm = moment("2018-01-01T00:00").format("YYYY-MM-DDTHH:mm");

I use: this.startTimeForm = moment().format();

The entire example with some extra functionalities is:

import { Component } from "@angular/core";
import moment from 'moment';

@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})

export class HomePage {

  //View variables
  startDateForm: string;
  startTimeForm: string;
  endTimeForm: string;
  //Calculate total time
  totalTime;
  //Final dates to upload to firebase
  start: string;
  end: string;

  constructor() {
    //Show the current date
    this.startDateForm = moment().format("YYYY-MM-DDTHH:mm"); 

    //Show the current time, but with 00 minutes
    this.startTimeForm = moment().minutes(0).format();
    this.endTimeForm = moment().minutes(0).format();
  }

  startDateChanged(startDateForm) {
    this.calculateAppointmentDate();
  }

  startTimeChanged(startTimeForm) {
    this.calculateAppointmentDate();
    this.calculateTotalTime();
  }

  endTimeChanged(endTimeForm) {
    this.calculateAppointmentDate();
    this.calculateTotalTime();
  }

  //Configure start and end dates before storing in DDBB with a "YYYY-MM-DDTHH:mm" format
  calculateAppointmentDate() {

    //Configure start = start date + start time
    this.start = moment(this.startDateForm).format("YYYY-MM-DD") + "T" + moment(this.startTimeForm).format("HH:mm");

    var begin = moment(this.startTimeForm);
    var finish = moment(this.endTimeForm);
    //If finish is before than begin, the time belogn to the next day
    if (finish.isBefore(begin)) {
      //Calculate the day after
      var nextDay = moment(this.startDateForm).add(1, 'days');
      //Generate the complete date and time for uploading to server
      this.end = nextDay.format("YYYY-MM-DD") + "T" + moment(this.endTimeForm).format("HH:mm");
    } else {
      //Create the format date
      this.end = moment(this.startDateForm).format("YYYY-MM-DD") + "T" + moment(this.endTimeForm).format("HH:mm");
    }
  }

  //Total time for the date in hours and minutes
  calculateTotalTime() {
    const begin = moment(this.start);
    const finish = moment(this.end);
    const diffDuration = moment.duration(finish.diff(begin));
    var hours = diffDuration.hours();
    var minutes = diffDuration.minutes();
    if (minutes != 0) {
      this.totalTime = hours + " horas " + minutes + " minutos"; //Showing hours and minutes
    } else {
      this.totalTime = hours + " horas"; //No minutes
    }
  }
}
<ion-content >
    <ion-grid>
        <ion-row text-left>
            <ion-col>
                <ion-datetime (ionChange)="startDateChanged(startDateForm)" [(ngModel)]="startDateForm" displayFormat="DDD DD/MM/YYYY" pickerFormat="DD/MMM/YYYY"></ion-datetime>
                <ion-label>Raw: {{startDateForm}}</ion-label>
            </ion-col>
            <ion-col>
                <ion-datetime (ionChange)="startTimeChanged(startTimeForm)" displayFormat="HH:mm" [(ngModel)]="startTimeForm">
                </ion-datetime>

                <ion-label>Raw: {{startTimeForm}}</ion-label>
            </ion-col>
            <ion-col>
                <ion-datetime (ionChange)="endTimeChanged(endTimeForm)" displayFormat="HH:mm" [(ngModel)]="endTimeForm">
                </ion-datetime>
                <ion-label>Raw: {{endTimeForm}}</ion-label>
            </ion-col>
        </ion-row>
    </ion-grid>

    <h2>Start string for DDBB: {{start}}</h2>

    <h2>End string for DDBB: {{end}}</h2>

    <h2>After downloading from DDBB: {{end}}</h2>
    <ion-item no-lines>
        <h2 *ngIf="start">
            {{start | date: 'EEEE d MMMM yyyy'}}
        </h2>
    </ion-item>
    <ion-item no-lines *ngIf="start&&end">
        <ion-label>
            {{start | date: 'HH:mm'}} a {{end | date: 'HH:mm'}}
        </ion-label>
    </ion-item>

    <!-- Total Time -->
    <ion-item *ngIf="totalTime" no-lines>
        <ion-label item-start>Duración Total:</ion-label>
        <!--TODO-->
        <ion-badge item-end>{{totalTime}}</ion-badge>
    </ion-item>

</ion-content>
jmondragon commented 6 years ago

I just wanted to add confirmation that formatting the incoming date correctly seems to resolve the issue.

Specifically, using moment().format() vs moment().format("YYYY-MM-DDTHH:mm")

In my case, I was transitioning from ionic AlertController with input fields to Modals with FormGroups. AlertController datetime input required the specific format, so my copy/paste code was messing with my timezones.

dnmd commented 6 years ago

Looked at the issue, and can confirm the findings from @tommck. After doing some research, it seems I've found the cause, at L210 the let tzOffset = 0; is initialized to naive. It should be let tzOffset = isPresent(parse[8]) ? 0 : null;, then, subsequently the check at L364 can be more specific. The snippet below shows the whole fix, as far as I was able to test it.

// datetime-util.ts
// @line 210
let tzOffset = isPresent(parse[8]) ? 0 : null

// @line 364
if (data.tzOffset === 0) {
  // YYYY-MM-DDTHH:mm:SSZ
  rtn += "Z";
} else if (!isBlank(data.tzOffset)) {
  // YYYY-MM-DDTHH:mm:SS+/-HH:mm
  rtn +=
    (data.tzOffset > 0 ? "+" : "-") +
    twoDigit(Math.floor(data.tzOffset / 60)) +
    ":" +
    twoDigit(data.tzOffset % 60);
}

Tested with the following input values, and modified e.g. the minutes, "2018-04-03T10:32:57", "2017-01-01T10:32:57-05:00" and "2017-01-01T10:32:57Z" where the output its timezone are preserved.

mariocalin commented 6 years ago

Same here. Is it going to be fixed soon?

Having to use other libraries to solve the issue with the "official" datepicker does not seem right.

alexsalesdev commented 6 years ago

I fixed it with this

moment(ion_datetime_value).local().format("YYYY-MM-DD[T]HH:mm:ss.000") + 'Z' 

the datatype I used for it is string.

ionitron-bot[bot] commented 5 years ago

This issue has been automatically identified as an Ionic 3 issue. We recently moved Ionic 3 to its own repository. I am moving this issue to the repository for Ionic 3. Please track this issue over there.

If I've made a mistake, and if this issue is still relevant to Ionic 4, please let the Ionic Framework team know!

Thank you for using Ionic!

ionitron-bot[bot] commented 5 years ago

Issue moved to: https://github.com/ionic-team/ionic-v3/issues/215