<commit_before>// IMPORTS =========================================================================================
import {assoc} from "ramda";
import Class from "classnames";
import Cycle from "cyclejs";
let {Rx} = Cycle;
let {Observable, Subject} = Rx;

// CONSTANTS =======================================================================================
const TICK_MS = 500;
const MINUTE_MS = 600 * TICK_MS;

// APP =============================================================================================
function Intent(interactions) {
  return {
    // TODO: debounce without delay
    trigger$: interactions.get(".btn.trigger", "click").map(true)
  }
}

function Model(intentions) {
  function seedState() {
    return {
      watch: 0,         // state: 0 = stopped, 1 = running, 2 = paused
      value: MINUTE_MS, // timer value in milliseconds
      prevValue: MINUTE_MS,
    };
  }

  let trigger$ = intentions.trigger$.map(() => {
    return function (state) {
      if (state.watch == 2) {
        state = assoc("prevValue", state.value, state);
        state = assoc("value", MINUTE_MS, state);
      }
      state = assoc("watch", (state.watch + 1) % 3, state);
      return state;
    }
  });

  let tick$ = Observable.interval(TICK_MS)
    .map(() => {
      return function (state) {
        if (state.watch == 1) {
          state = assoc("value", state.value > TICK_MS ? state.value - TICK_MS : 0, state);
        }
        if (state.value == 0) {
          state = assoc("watch", 0, state);
        }
        return state;
      }
    });

  let transform$ = Rx.Observable.merge(
    trigger$, tick$
  );

  return {
    state$: transform$
      .scan(seedState(), (state, transform) => transform(state))
      .map(state => {
        state = assoc("prevValue", MINUTE_MS - state.prevValue, state);
        state = assoc("value", MINUTE_MS - state.value, state);
        return state;
      })
      .distinctUntilChanged()
      .shareReplay(1)
  };
}

function View(state) {
  function mSecToSec(value) {
    return (value / 1000).toFixed(1)
  }

  function secondToAngle(second) {
    return (second % 60) * 6;
  }

  return state.state$.map(state => {
      let prevSecond = mSecToSec(state.prevValue);
      let second = mSecToSec(state.value);
      let angle = secondToAngle(second);
      let stopped = state.watch == 0;
      return (
        <div>
          <div className="stopWatch frame">
            <div className={Class("stopWatch", "arrow")} style={{
              "transform": `rotate(${angle}deg)`,
              "transform-origin": "bottom",
              "transition-property": "transform",
              "transition-timing-function": stopped ? "ease-out" : "cubic-bezier(.4, 2, .55, .44)",
              "transition-duration": stopped ? `${prevSecond / 30}s` : `${TICK_MS / 1000}s`,
            }}>
              {second}
            </div>
          </div>
          <div className="stopWatch trigger">
            <button className="btn btn-default trigger">Trigger</button>
          </div>
        </div>
      );
    }
  );
}

Cycle.applyToDOM("#main", interactions => View(Model(Intent(interactions))));<commit_msg>Refactor stopwatch<commit_after>// IMPORTS =========================================================================================
import {assoc} from "ramda";
import Class from "classnames";
import Cycle from "cyclejs";
let {Rx} = Cycle;
let {Observable, Subject} = Rx;

// CONSTANTS =======================================================================================
const TICK_MS = 500;
const MINUTE_MS = 60000;

// APP =============================================================================================
function Intent(interactions) {
  return {
    // TODO: debounce without delay
    trigger$: interactions.get(".btn.trigger", "click").map(true)
  }
}

function Model(intentions) {
  function seedState() {
    return {
      watch: 0,            // state: 0 = stopped, 1 = running, 2 = paused
      value: 0,            // timer value in milliseconds
      valueBeforeReset: 0, // prev timer value, required for animation
    };
  }

  let trigger$ = intentions.trigger$.map(() => {
    return function (state) {
      if (state.watch == 2) {
        state = assoc("valueBeforeReset", state.value, state);
        state = assoc("value", 0, state);
      }
      state = assoc("watch", (state.watch + 1) % 3, state);
      return state;
    }
  });

  let tick$ = Observable.interval(TICK_MS)
    .map(() => {
      return function (state) {
        if (state.watch == 1) {
          if (state.value < MINUTE_MS - TICK_MS) {
            state = assoc("value", state.value + TICK_MS, state);
          } else {
            state = assoc("value", 0, state);
          }
        }
        if (state.value == 0) {
          state = assoc("watch", 0, state);
        }
        return state;
      }
    });

  let transform$ = Rx.Observable.merge(
    trigger$, tick$
  );

  return {
    state$: transform$
      .scan(seedState(), (state, transform) => transform(state))
      .distinctUntilChanged()
      .shareReplay(1)
  };
}

function View(state) {
  function mSecToSec(value) {
    return (value / 1000).toFixed(1)
  }

  function secondToAngle(second) {
    return (second % 60) * 6;
  }

  return state.state$.map(state => {
      let secondBeforeReset = mSecToSec(state.valueBeforeReset);
      let second = mSecToSec(state.value);
      let angle = secondToAngle(second);
      let stopped = state.watch == 0;
      return (
        <div>
          <div className="stopWatch frame">
            <div className={Class("stopWatch", "arrow")} style={{
              "transform": `rotate(${angle}deg)`,
              "transform-origin": "bottom",
              "transition-property": "transform",
              "transition-timing-function": stopped ? "ease-out" : "cubic-bezier(.4, 2, .55, .44)",
              "transition-duration": stopped ? `${secondBeforeReset / 30}s` : `${TICK_MS / 1000}s`,
            }}>
              {second}
            </div>
          </div>
          <div className="stopWatch trigger">
            <button className="btn btn-default trigger">Trigger</button>
          </div>
        </div>
      );
    }
  );
}

Cycle.applyToDOM("#main", interactions => View(Model(Intent(interactions))));
{'fraction_non_alphanumeric': 0.22526515751147697, 'fraction_numerical': 0.013139148329903436, 'mean_word_length': 2.533557046979866, 'pattern_counts': {'":': 10, '<': 24, '<?xml version=': 0, '>': 35, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '954806', 'n_tokens_mistral': 1914, 'n_tokens_neox': 1791, 'n_words': 422}