rxjs — простая проблема from/start/duration

Я пытаюсь реализовать простую форму from/to/duration в rxjs для изучения.

Было бы намного проще просто использовать геттеры/сеттеры, но я хочу этому научиться.

Может ли кто-нибудь помочь мне найти простое решение с помощью rxjs для решения этой проблемы?

From : Start time of day
To : End time of day
Time : Duration between to and from
  • Если пользователь изменяет To, Time должно обновляться, если From не пусто.
  • Если пользователь изменяет Time, To должно обновляться, если только From не пусто.

Я считаю, что моя текущая реализация имеет много проблем, потому что она приводит к бесконечному циклу. Кроме того, похоже, что поскольку я подписываюсь много раз, я получаю наблюдаемые значения отложенным способом.

import { Component, OnInit } from '@angular/core';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { distinct, distinctUntilChanged, filter, map } from 'rxjs/operators';


export class Clock implements OnInit {

  fromSubject: BehaviorSubject<string> = new BehaviorSubject('');
  toSubject: BehaviorSubject<string> = new BehaviorSubject('');
  timeSubject: BehaviorSubject<string> = new BehaviorSubject('');

  from$: Observable<string>;
  to$: Observable<string>;
  time$: Observable<string>;
  seconds$: BehaviorSubject<number> = new BehaviorSubject(0);

  _from: string;
  _to: string;
  _time: string;

  get from(): string {
    return this._from;
  }
  set from(value: string) {
    this.fromSubject.next(value);
  }
  get to(): string {
    return this._to;
  }
  set to(value: string) {
    this.toSubject.next(value);
  }
  get time(): string {
    return this._time;
  }
  set time(value: string) {
    this.timeSubject.next(value);
  }

  ngOnInit() {

    this.from$ = this.fromSubject.pipe(
      distinctUntilChanged(),
      map(from => {
        if (!from) {
          return from;
        } else if (!isNaN(+from)) {
          return from + ':00';
        } else {
          const seconds = this.convertTimeToSeconds(from);
          return this.convertSecondsToTime(seconds);
        }
      })
    );

    this.to$ = this.toSubject.pipe(
      distinctUntilChanged(),
      map(to => {
        if (!to) {
          return to;
        } else if (!isNaN(+to)) {
          return to + ':00';
        } else {
          const seconds = this.convertTimeToSeconds(to);
          const txt = this.convertSecondsToTime(seconds);
          return txt;
        }
      })
    );

    this.time$ = this.timeSubject.pipe(
      distinctUntilChanged(),
      map(time => {
        if (!time) {
          return time;
        } else if (!isNaN(+time)) {
          return time + 'h';
        } else {
          const seconds = this.convertDurationToSeconds(time);
          console.log('TimeSeconds ' + seconds);
          return this.convertSecondsToDuration(seconds);
        }
      })
    );

    this.from$.subscribe(f => {
      this._from = f;
    });

    this.to$.subscribe(t => {
      this._to = t;
    });

    this.time$.subscribe(t => {
      this._time = t;
      const seconds = this.convertDurationToSeconds(t);
      this.seconds$.next(seconds);
    });

    combineLatest(this.from$, this.seconds$)
      .pipe(filter(([from, seconds]) => !!from && !!seconds))
      .subscribe(([from, seconds]) => {
        if (!from) {
          return;
        }
        const fromHours = this.getHours(from);
        const fromMins = this.getMinutes(from);

        const fromSeconds = fromHours * 3600 + fromMins * 60;
        const toSeconds = fromSeconds + seconds;

        const to = this.convertSecondsToTime(toSeconds);
        const time = this.convertSecondsToDuration(toSeconds - fromSeconds);

        this.toSubject.next(to);
        this.timeSubject.next(time);
      });

    combineLatest(this.from$, this.to$).subscribe(([from, to]) => {
      if (!from || !to) {
        return;
      }
      const fromHours = this.getHours(from);
      const toHours = this.getHours(to);
      const fromMins = this.getMinutes(from);
      const toMins = this.getMinutes(to);
      const seconds = (toHours - fromHours) * 3600 + (toMins - fromMins) * 60;
      this.seconds$.next(seconds);
    });

    const now = new Date();
    const hours = now.getHours();
    let minutes = now.getMinutes();
    minutes = minutes - (minutes % 15);
    this.to = hours + ':' + minutes;
    this.from = '8:00';
    this.to = '10:00';
  }

  convertDurationToSeconds = str => {
    let seconds = 0;
    const hours = str.match(/^(\d+)\s*(?:h|hours)/);
    const minutes = str.match(/^(?:\d+h)?(\d+)\s*(?:(?:m)|(?:min))?$/);
    if (hours) {
      seconds += parseInt(hours[1], 10) * 3600;
    }
    if (minutes) {
      seconds += parseInt(minutes[1], 10) * 60;
    }
    return seconds;
  }

  convertTimeToSeconds = str => {
    let seconds = 0;
    const hours = this.getHours(str);
    const minutes = this.getMinutes(str);
    if (hours) {
      seconds += hours * 3600;
    }
    if (minutes) {
      seconds += minutes * 60;
    }
    return seconds;
  }

  convertSecondsToDuration(seconds: number) {
    const durationHours = Math.floor(seconds / 3600);
    const durationMinutes = Math.floor((seconds - durationHours * 3600) / 60);
    let durationStr = '';
    if (durationHours > 0) {
      durationStr = durationHours + 'h';
    }
    if (durationMinutes > 0) {
      durationStr = durationStr + durationMinutes + 'm';
    }
    if (!durationStr) {
      durationStr = '0h';
    }
    console.log('durationStr ' + durationStr + ' seconds : ' + seconds);
    return durationStr;
  }

  convertSecondsToTime(seconds: number) {
    const durationHours = Math.floor(seconds / 3600);
    const durationMinutes = Math.floor((seconds - durationHours * 3600) / 60);
    const durationHoursStr = durationHours.toString();
    const durationMinutesStr =
      durationMinutes < 10 ? '0' + durationMinutes : durationMinutes;
    const timeStr = durationHoursStr + ':' + durationMinutesStr;
    console.log(seconds, durationHours, durationMinutes, timeStr);
    return timeStr;
  }

  getHours = str => {
    const hours = str.match(/^((?:(2[0-3])|(1[0-9])|0?[0-9])):(?:[0-5][0-9])/);
    if (hours) {
      return parseInt(hours[1], 10);
    }
    return 0;
  }

  getMinutes = str => {
    const minutes = str.match(
      /^(?:(?:2[0-3])|(?:1[0-9])|0?[0-9]):((?:[0-5][0-9])|[0-9])/
    );
    if (minutes) {
      return parseInt(minutes[1], 10);
    }
    return 0;
  }
}


person Ludovic C    schedule 28.05.2019    source источник


Ответы (1)


Я должен сказать, что в качестве «простого примера» RxJS вы наверняка выбрали вариант с какой-то неуклюжей бизнес-логикой. :) (Например, еще один пограничный случай: если пользователь меняет «от» и уже изменил время, остается ли время прежним, а «до» обновляется?).

Я стараюсь избегать BehaviorSubjects до тех пор, пока мне не понадобится их использовать, так что пока давайте начнем с Subjects и посмотрим, сработает ли это:

from = new Subject<string>();
to = new Subject<string>();
time = new Subject<string>();

from всегда устанавливается только из его ввода (я предполагаю, что пользовательский интерфейс). Итак, давайте просто определим его как Observable без дополнительной обработки:

from$ = this.from.asObservable()

to задается из его ввода или из потока "timeFrom" (извините, не смог придумать лучшего имени):

timeFrom$ = combineLatest(this.time, this.from).pipe(
    map([time, from] => time - from) // I'm leaving out formatting in this example
)

Прелесть combLatest заключается в том, что он удовлетворяет вашему требованию, что мы используем timeFrom только в том случае, если "from" имеет значение, так как combLatest не испускает, если оба потока не испустили. Таким образом, наш поток to$ становится:

to$ = merge(
    this.to,
    this.timeFrom$
)

Точно так же мы можем определить время как исходящее либо от его ввода, либо от потока «toFrom»:

toFrom$ = combineLatest(this.to, this.from).pipe(
    map([to, from] => from - to)
)

И это приводит к:

time$ = merge(
    this.time,
    this.toFrom$
)

Разбив проблему на 5 потоков вместо 3, мы теперь избегаем бесконечного цикла, о котором вы упоминаете.

Я надеюсь, что это имело смысл. Для простоты я убрал форматирование времени, которое у вас было.

person Jesse    schedule 28.05.2019