<commit_before>/*global defineSuite*/
defineSuite([
             'Core/Clock',
             'Core/ClockStep',
             'Core/ClockRange',
             'Core/JulianDate'
            ], function(
              Clock,
              ClockStep,
              ClockRange,
              JulianDate) {
    "use strict";
    /*global jasmine,describe,xdescribe,it,xit,expect,beforeEach,afterEach,beforeAll,afterAll,spyOn,runs,waits,waitsFor*/

    it('constructor sets default parameters', function() {
        var clock = new Clock();
        expect(clock.stopTime).toEqual(clock.startTime.addDays(1));
        expect(clock.startTime).toEqual(clock.currentTime);
        expect(clock.clockStep).toEqual(ClockStep.SYSTEM_CLOCK_MULTIPLIER);
        expect(clock.clockRange).toEqual(ClockRange.UNBOUNDED);
        expect(clock.multiplier).toEqual(1.0);
    });

    it('constructor sets provided parameters correctly', function() {
        var start = JulianDate.fromTotalDays(12);
        var stop = JulianDate.fromTotalDays(112);
        var currentTime = JulianDate.fromTotalDays(13);
        var step = ClockStep.TICK_DEPENDENT;
        var range = ClockRange.LOOP_STOP;
        var multiplier = 1.5;
        var clock = new Clock({
            currentTime : currentTime,
            clockStep : step,
            multiplier : multiplier,
            startTime : start,
            stopTime : stop,
            clockRange : range
        });
        expect(clock.startTime).toEqual(start);
        expect(clock.stopTime).toEqual(stop);
        expect(clock.currentTime).toEqual(currentTime);
        expect(clock.clockStep).toEqual(step);
        expect(clock.clockRange).toEqual(range);
        expect(clock.multiplier).toEqual(multiplier);
    });

    it('constructor works with no currentTime parameter', function() {
        var start = JulianDate.fromTotalDays(12);
        var stop = JulianDate.fromTotalDays(112);
        var currentTime = JulianDate.fromTotalDays(12);
        var step = ClockStep.TICK_DEPENDENT;
        var range = ClockRange.LOOP_STOP;
        var multiplier = 1.5;
        var clock = new Clock({
            clockStep : step,
            multiplier : multiplier,
            startTime : start,
            stopTime : stop,
            clockRange : range
        });
        expect(clock.startTime).toEqual(start);
        expect(clock.stopTime).toEqual(stop);
        expect(clock.currentTime).toEqual(currentTime);
        expect(clock.clockStep).toEqual(step);
        expect(clock.clockRange).toEqual(range);
        expect(clock.multiplier).toEqual(multiplier);
    });

    it('constructor works with no startTime parameter', function() {
        var stop = JulianDate.fromTotalDays(112);
        var currentTime = JulianDate.fromTotalDays(13);
        var step = ClockStep.TICK_DEPENDENT;
        var range = ClockRange.LOOP_STOP;
        var multiplier = 1.5;
        var clock = new Clock({
            currentTime : currentTime,
            clockStep : step,
            multiplier : multiplier,
            stopTime : stop,
            clockRange : range
        });
        expect(clock.startTime).toEqual(clock.currentTime);
        expect(clock.stopTime).toEqual(stop);
        expect(clock.currentTime).toEqual(currentTime);
        expect(clock.clockStep).toEqual(step);
        expect(clock.clockRange).toEqual(range);
        expect(clock.multiplier).toEqual(multiplier);
    });

    it('constructor works with no start or stop time', function() {
        var start = JulianDate.fromTotalDays(12);
        var stop = JulianDate.fromTotalDays(13);
        var currentTime = JulianDate.fromTotalDays(12);
        var step = ClockStep.TICK_DEPENDENT;
        var range = ClockRange.LOOP_STOP;
        var multiplier = 1.5;
        var clock = new Clock({
            currentTime : currentTime,
            clockStep : step,
            multiplier : multiplier,
            clockRange : range
        });
        expect(clock.startTime).toEqual(start);
        expect(clock.stopTime).toEqual(stop);
        expect(clock.currentTime).toEqual(currentTime);
        expect(clock.clockStep).toEqual(step);
        expect(clock.clockRange).toEqual(range);
        expect(clock.multiplier).toEqual(multiplier);
    });

    it('constructor works with no start or current time', function() {
        var start = JulianDate.fromTotalDays(12);
        var stop = JulianDate.fromTotalDays(13);
        var currentTime = JulianDate.fromTotalDays(12);
        var step = ClockStep.TICK_DEPENDENT;
        var range = ClockRange.LOOP_STOP;
        var multiplier = 1.5;
        var clock = new Clock({
            clockStep : step,
            multiplier : multiplier,
            stopTime : stop,
            clockRange : range
        });
        expect(clock.startTime).toEqual(start);
        expect(clock.stopTime).toEqual(stop);
        expect(clock.currentTime).toEqual(currentTime);
        expect(clock.clockStep).toEqual(step);
        expect(clock.clockRange).toEqual(range);
        expect(clock.multiplier).toEqual(multiplier);
    });

    it('constructor works with no current or stop time', function() {
        var start = JulianDate.fromTotalDays(12);
        var stop = JulianDate.fromTotalDays(13);
        var currentTime = JulianDate.fromTotalDays(12);
        var step = ClockStep.TICK_DEPENDENT;
        var range = ClockRange.LOOP_STOP;
        var multiplier = 1.5;
        var clock = new Clock({
            clockStep : step,
            multiplier : multiplier,
            startTime : start,
            clockRange : range
        });
        expect(clock.startTime).toEqual(start);
        expect(clock.stopTime).toEqual(stop);
        expect(clock.currentTime).toEqual(currentTime);
        expect(clock.clockStep).toEqual(step);
        expect(clock.clockRange).toEqual(range);
        expect(clock.multiplier).toEqual(multiplier);
    });

    it('constructor works with no stopTime parameter', function() {
        var start = JulianDate.fromTotalDays(12);
        var stop = JulianDate.fromTotalDays(13);
        var currentTime = JulianDate.fromTotalDays(12);
        var step = ClockStep.TICK_DEPENDENT;
        var range = ClockRange.LOOP_STOP;
        var multiplier = 1.5;
        var clock = new Clock({
            currentTime : currentTime,
            clockStep : step,
            multiplier : multiplier,
            startTime : start,
            clockRange : range
        });
        expect(clock.startTime).toEqual(start);
        expect(clock.stopTime).toEqual(stop);
        expect(clock.currentTime).toEqual(currentTime);
        expect(clock.clockStep).toEqual(step);
        expect(clock.clockRange).toEqual(range);
        expect(clock.multiplier).toEqual(multiplier);
    });

    it('Tick dependant clock step works animating forward.', function() {
        var start = JulianDate.fromTotalDays(0);
        var stop = JulianDate.fromTotalDays(1);
        var currentTime = JulianDate.fromTotalDays(0.5);
        var step = ClockStep.TICK_DEPENDENT;
        var range = ClockRange.LOOP_STOP;
        var multiplier = 1.5;
        var clock = new Clock({
            currentTime : currentTime,
            clockStep : step,
            multiplier : multiplier,
            startTime : start,
            stopTime : stop,
            clockRange : range
        });
        expect(clock.currentTime).toEqual(currentTime);

        currentTime = currentTime.addSeconds(multiplier);
        expect(currentTime).toEqual(clock.tick());
        expect(clock.currentTime).toEqual(currentTime);

        currentTime = currentTime.addSeconds(multiplier);
        expect(currentTime).toEqual(clock.tick());
        expect(clock.currentTime).toEqual(currentTime);
    });

    it('Tick dependant clock step works animating backwards.', function() {
        var start = JulianDate.fromTotalDays(0);
        var stop = JulianDate.fromTotalDays(1);
        var currentTime = JulianDate.fromTotalDays(0.5);
        var step = ClockStep.TICK_DEPENDENT;
        var range = ClockRange.LOOP_STOP;
        var multiplier = -1.5;
        var clock = new Clock({
            currentTime : currentTime,
            clockStep : step,
            multiplier : multiplier,
            startTime : start,
            stopTime : stop,
            clockRange : range
        });
        expect(clock.currentTime).toEqual(currentTime);

        currentTime = currentTime.addSeconds(multiplier);
        expect(currentTime).toEqual(clock.tick());
        expect(clock.currentTime).toEqual(currentTime);

        currentTime = currentTime.addSeconds(multiplier);
        expect(currentTime).toEqual(clock.tick());
        expect(clock.currentTime).toEqual(currentTime);
    });

    it('Tick dependant clock step works unbounded animating forward.', function() {
        var start = JulianDate.fromTotalDays(0);
        var stop = JulianDate.fromTotalDays(1);
        var currentTime = JulianDate.fromTotalDays(1);
        var step = ClockStep.TICK_DEPENDENT;
        var range = ClockRange.UNBOUNDED;
        var multiplier = 1.5;
        var clock = new Clock({
            currentTime : currentTime,
            clockStep : step,
            multiplier : multiplier,
            startTime : start,
            stopTime : stop,
            clockRange : range
        });
        expect(clock.currentTime).toEqual(currentTime);

        currentTime = currentTime.addSeconds(multiplier);
        expect(currentTime).toEqual(clock.tick());
        expect(clock.currentTime).toEqual(currentTime);

        currentTime = currentTime.addSeconds(multiplier);
        expect(currentTime).toEqual(clock.tick());
        expect(clock.currentTime).toEqual(currentTime);
    });

    it('Tick dependant clock step works unbounded animating backwards.', function() {
        var start = JulianDate.fromTotalDays(0);
        var stop = JulianDate.fromTotalDays(1);
        var currentTime = JulianDate.fromTotalDays(0);
        var step = ClockStep.TICK_DEPENDENT;
        var range = ClockRange.UNBOUNDED;
        var multiplier = -1.5;
        var clock = new Clock({
            currentTime : currentTime,
            clockStep : step,
            multiplier : multiplier,
            startTime : start,
            stopTime : stop,
            clockRange : range
        });
        expect(clock.currentTime).toEqual(currentTime);

        currentTime = currentTime.addSeconds(multiplier);
        expect(currentTime).toEqual(clock.tick());
        expect(clock.currentTime).toEqual(currentTime);

        currentTime = currentTime.addSeconds(multiplier);
        expect(currentTime).toEqual(clock.tick());
        expect(clock.currentTime).toEqual(currentTime);
    });

    it('Tick dependant clock loops animating forward.', function() {
        var start = JulianDate.fromTotalDays(0);
        var stop = JulianDate.fromTotalDays(1);
        var currentTime = JulianDate.fromTotalDays(1);
        var step = ClockStep.TICK_DEPENDENT;
        var range = ClockRange.LOOP_STOP;
        var multiplier = 1.5;
        var clock = new Clock({
            currentTime : currentTime,
            clockStep : step,
            multiplier : multiplier,
            startTime : start,
            stopTime : stop,
            clockRange : range
        });
        expect(clock.currentTime).toEqual(currentTime);

        currentTime = start.addSeconds(multiplier);
        expect(currentTime).toEqual(clock.tick());
        expect(clock.currentTime).toEqual(currentTime);

        currentTime = currentTime.addSeconds(multiplier);
        expect(currentTime).toEqual(clock.tick());
        expect(clock.currentTime).toEqual(currentTime);
    });

    it('Tick dependant clock step stops at start when animating backwards with ClockRange.LOOP_STOP.', function() {
        var start = JulianDate.fromTotalDays(0);
        var stop = JulianDate.fromTotalDays(1);

        var currentTime = JulianDate.fromTotalDays(0);
        var step = ClockStep.TICK_DEPENDENT;
        var range = ClockRange.LOOP_STOP;
        var multiplier = -100.0;
        var clock = new Clock({
            currentTime : currentTime,
            clockStep : step,
            multiplier : multiplier,
            startTime : start,
            stopTime : stop,
            clockRange : range
        });

        expect(clock.currentTime).toEqual(currentTime);
        expect(start).toEqual(clock.tick());
        expect(start).toEqual(clock.currentTime);
    });

    it('Tick dependant clock step stops at end when animating .', function() {
        var start = JulianDate.fromTotalDays(0);
        var stop = JulianDate.fromTotalDays(1);

        var currentTime = JulianDate.fromTotalDays(1);
        var step = ClockStep.TICK_DEPENDENT;
        var range = ClockRange.CLAMPED;
        var multiplier = 100.0;
        var clock = new Clock({
            currentTime : currentTime,
            clockStep : step,
            multiplier : multiplier,
            startTime : start,
            stopTime : stop,
            clockRange : range
        });

        expect(clock.currentTime).toEqual(currentTime);
        expect(stop).toEqual(clock.tick());
        expect(stop).toEqual(clock.currentTime);
    });

    it('Ticks in real-time.', function() {
        //We can't numerically validate the real-time clock, but we
        //can at least make sure the code executes.
        var clock = new Clock({
            clockStep : ClockStep.SYSTEM_DEPENDENT
        });
        var time1 = clock.tick();

        waitsFor(function() {
            var time2 = clock.tick();
            return time2.greaterThan(time1);
        });
    });

    it('Tick dependant clock step stops at start animating backwards.', function() {
        var start = JulianDate.fromTotalDays(0);
        var stop = JulianDate.fromTotalDays(1);

        var currentTime = JulianDate.fromTotalDays(0);
        var step = ClockStep.TICK_DEPENDENT;
        var range = ClockRange.CLAMPED;
        var multiplier = -100.0;
        var clock = new Clock({
            currentTime : currentTime,
            clockStep : step,
            multiplier : multiplier,
            startTime : start,
            stopTime : stop,
            clockRange : range
        });

        expect(clock.currentTime).toEqual(currentTime);
        expect(start).toEqual(clock.tick());
        expect(start).toEqual(clock.currentTime);
    });

    it('Passing parameter to tick ticks that many seconds.', function() {
        var start = JulianDate.fromTotalDays(0);
        var stop = JulianDate.fromTotalDays(1);
        var currentTime = JulianDate.fromTotalDays(0);
        var step = ClockStep.SYSTEM_CLOCK_MULTIPLIER;
        var range = ClockRange.UNBOUNDED;
        var multiplier = 10000;
        var clock = new Clock({
            currentTime : currentTime,
            clockStep : step,
            multiplier : multiplier,
            startTime : start,
            stopTime : stop,
            clockRange : range
        });

        expect(clock.currentTime).toEqual(currentTime);
        currentTime = currentTime.addSeconds(5);
        clock.tick(5);
        expect(clock.currentTime).toEqual(currentTime);
        clock.tick(-5);
        currentTime = currentTime.addSeconds(-5);
        expect(clock.currentTime).toEqual(currentTime);
    });

    it('throws if start time is after stop time.', function() {
        var start = JulianDate.fromTotalDays(1);
        var stop = JulianDate.fromTotalDays(0);
        expect(function() {
            return new Clock({
                startTime : start,
                stopTime : stop
            });
        }).toThrow();
    });
});
<commit_msg>Fix problems found during review.<commit_after>/*global defineSuite*/
defineSuite([
             'Core/Clock',
             'Core/ClockStep',
             'Core/ClockRange',
             'Core/JulianDate'
            ], function(
              Clock,
              ClockStep,
              ClockRange,
              JulianDate) {
    "use strict";
    /*global jasmine,describe,xdescribe,it,xit,expect,beforeEach,afterEach,beforeAll,afterAll,spyOn,runs,waits,waitsFor*/

    it('constructor sets default parameters', function() {
        var clock = new Clock();
        expect(clock.stopTime).toEqual(clock.startTime.addDays(1));
        expect(clock.startTime).toEqual(clock.currentTime);
        expect(clock.clockStep).toEqual(ClockStep.SYSTEM_CLOCK_MULTIPLIER);
        expect(clock.clockRange).toEqual(ClockRange.UNBOUNDED);
        expect(clock.multiplier).toEqual(1.0);
    });

    it('constructor sets provided parameters correctly', function() {
        var start = JulianDate.fromTotalDays(12);
        var stop = JulianDate.fromTotalDays(112);
        var currentTime = JulianDate.fromTotalDays(13);
        var step = ClockStep.TICK_DEPENDENT;
        var range = ClockRange.LOOP_STOP;
        var multiplier = 1.5;
        var clock = new Clock({
            currentTime : currentTime,
            clockStep : step,
            multiplier : multiplier,
            startTime : start,
            stopTime : stop,
            clockRange : range
        });
        expect(clock.startTime).toEqual(start);
        expect(clock.stopTime).toEqual(stop);
        expect(clock.currentTime).toEqual(currentTime);
        expect(clock.clockStep).toEqual(step);
        expect(clock.clockRange).toEqual(range);
        expect(clock.multiplier).toEqual(multiplier);
    });

    it('constructor works with no currentTime parameter', function() {
        var start = JulianDate.fromTotalDays(12);
        var stop = JulianDate.fromTotalDays(112);
        var currentTime = JulianDate.fromTotalDays(12);
        var step = ClockStep.TICK_DEPENDENT;
        var range = ClockRange.LOOP_STOP;
        var multiplier = 1.5;
        var clock = new Clock({
            clockStep : step,
            multiplier : multiplier,
            startTime : start,
            stopTime : stop,
            clockRange : range
        });
        expect(clock.startTime).toEqual(start);
        expect(clock.stopTime).toEqual(stop);
        expect(clock.currentTime).toEqual(currentTime);
        expect(clock.clockStep).toEqual(step);
        expect(clock.clockRange).toEqual(range);
        expect(clock.multiplier).toEqual(multiplier);
    });

    it('constructor works with no startTime parameter', function() {
        var stop = JulianDate.fromTotalDays(112);
        var currentTime = JulianDate.fromTotalDays(13);
        var step = ClockStep.TICK_DEPENDENT;
        var range = ClockRange.LOOP_STOP;
        var multiplier = 1.5;
        var clock = new Clock({
            currentTime : currentTime,
            clockStep : step,
            multiplier : multiplier,
            stopTime : stop,
            clockRange : range
        });
        expect(clock.startTime).toEqual(clock.currentTime);
        expect(clock.stopTime).toEqual(stop);
        expect(clock.currentTime).toEqual(currentTime);
        expect(clock.clockStep).toEqual(step);
        expect(clock.clockRange).toEqual(range);
        expect(clock.multiplier).toEqual(multiplier);
    });

    it('constructor works with no start or stop time', function() {
        var start = JulianDate.fromTotalDays(12);
        var stop = JulianDate.fromTotalDays(13);
        var currentTime = JulianDate.fromTotalDays(12);
        var step = ClockStep.TICK_DEPENDENT;
        var range = ClockRange.LOOP_STOP;
        var multiplier = 1.5;
        var clock = new Clock({
            currentTime : currentTime,
            clockStep : step,
            multiplier : multiplier,
            clockRange : range
        });
        expect(clock.startTime).toEqual(start);
        expect(clock.stopTime).toEqual(stop);
        expect(clock.currentTime).toEqual(currentTime);
        expect(clock.clockStep).toEqual(step);
        expect(clock.clockRange).toEqual(range);
        expect(clock.multiplier).toEqual(multiplier);
    });

    it('constructor works with no start or current time', function() {
        var start = JulianDate.fromTotalDays(12);
        var stop = JulianDate.fromTotalDays(13);
        var currentTime = JulianDate.fromTotalDays(12);
        var step = ClockStep.TICK_DEPENDENT;
        var range = ClockRange.LOOP_STOP;
        var multiplier = 1.5;
        var clock = new Clock({
            clockStep : step,
            multiplier : multiplier,
            stopTime : stop,
            clockRange : range
        });
        expect(clock.startTime).toEqual(start);
        expect(clock.stopTime).toEqual(stop);
        expect(clock.currentTime).toEqual(currentTime);
        expect(clock.clockStep).toEqual(step);
        expect(clock.clockRange).toEqual(range);
        expect(clock.multiplier).toEqual(multiplier);
    });

    it('constructor works with no current or stop time', function() {
        var start = JulianDate.fromTotalDays(12);
        var stop = JulianDate.fromTotalDays(13);
        var currentTime = JulianDate.fromTotalDays(12);
        var step = ClockStep.TICK_DEPENDENT;
        var range = ClockRange.LOOP_STOP;
        var multiplier = 1.5;
        var clock = new Clock({
            clockStep : step,
            multiplier : multiplier,
            startTime : start,
            clockRange : range
        });
        expect(clock.startTime).toEqual(start);
        expect(clock.stopTime).toEqual(stop);
        expect(clock.currentTime).toEqual(currentTime);
        expect(clock.clockStep).toEqual(step);
        expect(clock.clockRange).toEqual(range);
        expect(clock.multiplier).toEqual(multiplier);
    });

    it('constructor works with no stopTime parameter', function() {
        var start = JulianDate.fromTotalDays(12);
        var stop = JulianDate.fromTotalDays(13);
        var currentTime = JulianDate.fromTotalDays(12);
        var step = ClockStep.TICK_DEPENDENT;
        var range = ClockRange.LOOP_STOP;
        var multiplier = 1.5;
        var clock = new Clock({
            currentTime : currentTime,
            clockStep : step,
            multiplier : multiplier,
            startTime : start,
            clockRange : range
        });
        expect(clock.startTime).toEqual(start);
        expect(clock.stopTime).toEqual(stop);
        expect(clock.currentTime).toEqual(currentTime);
        expect(clock.clockStep).toEqual(step);
        expect(clock.clockRange).toEqual(range);
        expect(clock.multiplier).toEqual(multiplier);
    });

    it('Tick dependant clock step works animating forward.', function() {
        var start = JulianDate.fromTotalDays(0);
        var stop = JulianDate.fromTotalDays(1);
        var currentTime = JulianDate.fromTotalDays(0.5);
        var step = ClockStep.TICK_DEPENDENT;
        var range = ClockRange.LOOP_STOP;
        var multiplier = 1.5;
        var clock = new Clock({
            currentTime : currentTime,
            clockStep : step,
            multiplier : multiplier,
            startTime : start,
            stopTime : stop,
            clockRange : range
        });
        expect(clock.currentTime).toEqual(currentTime);

        currentTime = currentTime.addSeconds(multiplier);
        expect(currentTime).toEqual(clock.tick());
        expect(clock.currentTime).toEqual(currentTime);

        currentTime = currentTime.addSeconds(multiplier);
        expect(currentTime).toEqual(clock.tick());
        expect(clock.currentTime).toEqual(currentTime);
    });

    it('Tick dependant clock step works animating backwards.', function() {
        var start = JulianDate.fromTotalDays(0);
        var stop = JulianDate.fromTotalDays(1);
        var currentTime = JulianDate.fromTotalDays(0.5);
        var step = ClockStep.TICK_DEPENDENT;
        var range = ClockRange.LOOP_STOP;
        var multiplier = -1.5;
        var clock = new Clock({
            currentTime : currentTime,
            clockStep : step,
            multiplier : multiplier,
            startTime : start,
            stopTime : stop,
            clockRange : range
        });
        expect(clock.currentTime).toEqual(currentTime);

        currentTime = currentTime.addSeconds(multiplier);
        expect(currentTime).toEqual(clock.tick());
        expect(clock.currentTime).toEqual(currentTime);

        currentTime = currentTime.addSeconds(multiplier);
        expect(currentTime).toEqual(clock.tick());
        expect(clock.currentTime).toEqual(currentTime);
    });

    it('Tick dependant clock step works unbounded animating forward.', function() {
        var start = JulianDate.fromTotalDays(0);
        var stop = JulianDate.fromTotalDays(1);
        var currentTime = JulianDate.fromTotalDays(1);
        var step = ClockStep.TICK_DEPENDENT;
        var range = ClockRange.UNBOUNDED;
        var multiplier = 1.5;
        var clock = new Clock({
            currentTime : currentTime,
            clockStep : step,
            multiplier : multiplier,
            startTime : start,
            stopTime : stop,
            clockRange : range
        });
        expect(clock.currentTime).toEqual(currentTime);

        currentTime = currentTime.addSeconds(multiplier);
        expect(currentTime).toEqual(clock.tick());
        expect(clock.currentTime).toEqual(currentTime);

        currentTime = currentTime.addSeconds(multiplier);
        expect(currentTime).toEqual(clock.tick());
        expect(clock.currentTime).toEqual(currentTime);
    });

    it('Tick dependant clock step works unbounded animating backwards.', function() {
        var start = JulianDate.fromTotalDays(0);
        var stop = JulianDate.fromTotalDays(1);
        var currentTime = JulianDate.fromTotalDays(0);
        var step = ClockStep.TICK_DEPENDENT;
        var range = ClockRange.UNBOUNDED;
        var multiplier = -1.5;
        var clock = new Clock({
            currentTime : currentTime,
            clockStep : step,
            multiplier : multiplier,
            startTime : start,
            stopTime : stop,
            clockRange : range
        });
        expect(clock.currentTime).toEqual(currentTime);

        currentTime = currentTime.addSeconds(multiplier);
        expect(currentTime).toEqual(clock.tick());
        expect(clock.currentTime).toEqual(currentTime);

        currentTime = currentTime.addSeconds(multiplier);
        expect(currentTime).toEqual(clock.tick());
        expect(clock.currentTime).toEqual(currentTime);
    });

    it('Tick dependant clock loops animating forward.', function() {
        var start = JulianDate.fromTotalDays(0);
        var stop = JulianDate.fromTotalDays(1);
        var currentTime = JulianDate.fromTotalDays(1);
        var step = ClockStep.TICK_DEPENDENT;
        var range = ClockRange.LOOP_STOP;
        var multiplier = 1.5;
        var clock = new Clock({
            currentTime : currentTime,
            clockStep : step,
            multiplier : multiplier,
            startTime : start,
            stopTime : stop,
            clockRange : range
        });
        expect(clock.currentTime).toEqual(currentTime);

        currentTime = start.addSeconds(multiplier);
        expect(currentTime).toEqual(clock.tick());
        expect(clock.currentTime).toEqual(currentTime);

        currentTime = currentTime.addSeconds(multiplier);
        expect(currentTime).toEqual(clock.tick());
        expect(clock.currentTime).toEqual(currentTime);
    });

    it('Tick dependant clock step stops at start when animating backwards with ClockRange.LOOP_STOP.', function() {
        var start = JulianDate.fromTotalDays(0);
        var stop = JulianDate.fromTotalDays(1);

        var currentTime = JulianDate.fromTotalDays(0);
        var step = ClockStep.TICK_DEPENDENT;
        var range = ClockRange.LOOP_STOP;
        var multiplier = -100.0;
        var clock = new Clock({
            currentTime : currentTime,
            clockStep : step,
            multiplier : multiplier,
            startTime : start,
            stopTime : stop,
            clockRange : range
        });

        expect(clock.currentTime).toEqual(currentTime);
        expect(start).toEqual(clock.tick());
        expect(start).toEqual(clock.currentTime);
    });

    it('Tick dependant clock step stops at end when animating forwards.', function() {
        var start = JulianDate.fromTotalDays(0);
        var stop = JulianDate.fromTotalDays(1);

        var currentTime = JulianDate.fromTotalDays(1);
        var step = ClockStep.TICK_DEPENDENT;
        var range = ClockRange.CLAMPED;
        var multiplier = 100.0;
        var clock = new Clock({
            currentTime : currentTime,
            clockStep : step,
            multiplier : multiplier,
            startTime : start,
            stopTime : stop,
            clockRange : range
        });

        expect(clock.currentTime).toEqual(currentTime);
        expect(stop).toEqual(clock.tick());
        expect(stop).toEqual(clock.currentTime);
    });

    it('Ticks in real-time.', function() {
        //We can't numerically validate the real-time clock, but we
        //can at least make sure the code executes.
        var clock = new Clock({
            clockStep : ClockStep.SYSTEM_CLOCK
        });
        var time1 = clock.tick();

        waitsFor(function() {
            var time2 = clock.tick();
            return time2.greaterThan(time1);
        });
    });

    it('Tick dependant clock step stops at start animating backwards.', function() {
        var start = JulianDate.fromTotalDays(0);
        var stop = JulianDate.fromTotalDays(1);

        var currentTime = JulianDate.fromTotalDays(0);
        var step = ClockStep.TICK_DEPENDENT;
        var range = ClockRange.CLAMPED;
        var multiplier = -100.0;
        var clock = new Clock({
            currentTime : currentTime,
            clockStep : step,
            multiplier : multiplier,
            startTime : start,
            stopTime : stop,
            clockRange : range
        });

        expect(clock.currentTime).toEqual(currentTime);
        expect(start).toEqual(clock.tick());
        expect(start).toEqual(clock.currentTime);
    });

    it('Passing parameter to tick ticks that many seconds.', function() {
        var start = JulianDate.fromTotalDays(0);
        var stop = JulianDate.fromTotalDays(1);
        var currentTime = JulianDate.fromTotalDays(0);
        var step = ClockStep.SYSTEM_CLOCK_MULTIPLIER;
        var range = ClockRange.UNBOUNDED;
        var multiplier = 10000;
        var clock = new Clock({
            currentTime : currentTime,
            clockStep : step,
            multiplier : multiplier,
            startTime : start,
            stopTime : stop,
            clockRange : range
        });

        expect(clock.currentTime).toEqual(currentTime);
        currentTime = currentTime.addSeconds(5);
        clock.tick(5);
        expect(clock.currentTime).toEqual(currentTime);
        clock.tick(-5);
        currentTime = currentTime.addSeconds(-5);
        expect(clock.currentTime).toEqual(currentTime);
    });

    it('throws if start time is after stop time.', function() {
        var start = JulianDate.fromTotalDays(1);
        var stop = JulianDate.fromTotalDays(0);
        expect(function() {
            return new Clock({
                startTime : start,
                stopTime : stop
            });
        }).toThrow();
    });
});
<|endoftext|>


<commit_before>// === UI stuff ===
//TODO visualize the alchemicals in the row headers of the table
//TODO add pictures for the ingredients in the column headers of the table
//TODO add pictures for the ingredients in the radio buttons
//TODO add pictures of the potions for the check boxes
//TODO add refresh indicators http://www.material-ui.com/#/components/refresh-indicator
//TODO downsize columns (and rows)

// === Additional views ===
//TODO add view to see individual worlds when you have just a few

// === Other stuff ===
//TODO add other fact types (periscope / beginner debunking
//TODO make the view match the state for the buttons etc.
//TODO fix lag issues

// The expected number of bits of information from real science is
// -[1/7*lg(1/7) * 7]
// = 2.807 bits

// The expected number of bits of information from experimenting randomly on an adventurer is
// -[1/7*lg(1/7) + 2/7*lg(2/7) + 1/7*lg(1/7) + 3/7*lg(3/7)]
// = 1.842 bits

import React from 'react';
// import logo from './logo.svg';
import './App.css';

import _ from 'lodash'
import update from 'react-addons-update'
import PureRenderMixin from 'react-addons-pure-render-mixin';

import {Table, TableBody, TableHeader, TableHeaderColumn, TableRow, TableRowColumn} from 'material-ui/Table';
import RaisedButton from 'material-ui/RaisedButton';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';

const style = {
  margin: 12,
};

// This is what an Alchemical looks like:
// [1, 1, -1]

// Here are all of the Alchemicals:
var alchemicals = [
  [-1, +1, -1],
  [+1, -1, +1],
  [+1, -1, -1],
  [-1, +1, +1],
  [-1, -1, +1],
  [+1, +1, -1],
  [-1, -1, -1],
  [+1, +1, +1],
]

var ingredients = [
  "Mushroom",
  "Fern",
  "Toad",
  "Birdclaw",
  "Flower",
  "Mandrake",
  "Scorpion",
  "Feather",
]

var potions = {
  "Red+":   [+1, 0, 0],
  "Red-":   [-1, 0, 0],
  "Green+": [0, +1, 0],
  "Green-": [0, -1, 0],
  "Blue+":  [0, 0, +1],
  "Blue-":  [0, 0, -1],
  "Soup":   [0, 0, 0],
}

var ReactFact = React.createClass({
  mixins: [PureRenderMixin],

  render: function() {
    return

      {this.props.index}: {JSON.stringify(this.props.item)}
      <RaisedButton onClick={this.props.deleteFact} value={this.props.index} style={style}>Delete</RaisedButton>

  }
});

var FactList = React.createClass({
  mixins: [PureRenderMixin],

  render: function() {
    return
{this.props.items.map((fact, factIndex) => <ReactFact key={factIndex} item={fact} deleteFact={_.bind(function(){this.props.deleteFact(factIndex)}, this)}/>)}
;
  }
});

var Ingredient = React.createClass({
  mixins: [PureRenderMixin],

  render: function() {
    return
<input type="radio" name="ingredient" onChange={this.props.callback}/>{this.props.name}

  }
})

var Potion = React.createClass({
  mixins: [PureRenderMixin],

  render: function() {
    return
<input type="checkbox" name="potion" onChange={this.props.callback}/>{this.props.name}

  }
})

var SheetCell = React.createClass({
  mixins: [PureRenderMixin],

  render: function() {
    return <TableRowColumn>{this.percentage()}</TableRowColumn>
  },
  percentage: function() {
    var worlds = _.filter(this.props.worlds, _.bind(function(world) {
      return _.isEqual(world[this.props.ingredientIndex], this.props.alchemical)
    }, this))
    return Math.round(100 * worlds.length / this.props.worlds.length, 0)
  }
})

var SheetRow = React.createClass({
  mixins: [PureRenderMixin],

  render: function() {
    return (
      <TableRow>
        <TableRowColumn>{this.props.alchemical}</TableRowColumn>
        {ingredients.map((ingredient, index) => <SheetCell ingredientIndex={index} alchemical={this.props.alchemical} key={index} worlds={this.props.worlds}/>)}
      </TableRow>
    )
  }
})

var AlchemistsSolverApp = React.createClass({
  mixins: [PureRenderMixin],

  getInitialState: function() {
    return {
      factlist: [],
      currentFact: this.makeNewFact(),
    }
  },
  makeNewFact: function() {
    return new TwoIngredientFact([0, 1], [false, false, false, false, false, false, false])
  },
  handleSubmit: function(e) {
    e.preventDefault();
    this.setState({
      factlist: this.state.factlist.concat([this.state.currentFact]),
    });
  },
  ingredientChange: function(ingredientIndex, ingredient) {
    var newIngredients = [];
    newIngredients[ingredientIndex] = ingredient;
    var newState = update(this.state,
      {currentFact: {ingredients: {$merge: newIngredients}}})
    this.setState(newState)
  },
  potionChange: function(potionIndex) {
    var newPossibleResults = [];
    newPossibleResults[potionIndex] = !this.state.currentFact.possibleResults[potionIndex];
    var newState = update(this.state,
      {currentFact: {possibleResults: {$merge: newPossibleResults}}})
    this.setState(newState)
  },
  render: function() {
    var self = this;

    var worlds = permutator(alchemicals)
    for (var factIndex in this.state.factlist) {
      var fact = this.state.factlist[factIndex]
      worlds = _.filter(worlds, _.bind(fact.check, fact))
    }

    return (
      <MuiThemeProvider>

Alchemists Solver

          <FactList items={this.state.factlist} deleteFact={this.deleteFact}/>

          <form action="" style={{display: "inline-block"}}>
            {ingredients.map((name, index) => <Ingredient name={name} ingredientNumber={0} key={index} callback={function() {self.ingredientChange(0, index)}} />)}
          </form>

          <form action="" style={{display: "inline-block"}}>
            {ingredients.map((name, index) => <Ingredient name={name} ingredientNumber={1} key={index} callback={function() {self.ingredientChange(1, index)}} />)}
          </form>

          <form action="" style={{display: "inline-block"}}>
            {_.keys(potions).map((name, index) => <Potion name={name} key={index} callback={function() {self.potionChange(index)}} />)}
          </form>

          <RaisedButton label="Add Fact" primary={true} style={style} onClick={this.handleSubmit}/>

Remaining worlds: {worlds.length}

            <TableHeader displaySelectAll={false} adjustForCheckbox={false}>
              <TableRow>
                <TableHeaderColumn/>
                {ingredients.map((name, index) => <TableHeaderColumn key={index}>{name}</TableHeaderColumn>)}
              </TableRow>
            </TableHeader>
            <TableBody>
              {alchemicals.map((alchemical, index) => <SheetRow alchemical={alchemical} key={index} worlds={worlds}/>)}
            </TableBody>

      </MuiThemeProvider>
    );
  },
  deleteFact: function(deleteIndex) {
    this.setState({factlist: removeAtIndex(this.state.factlist, deleteIndex)})
  },
});

// non-mutating
function removeAtIndex(arr, index) {
  return _.filter(arr, function(val, idx) {return idx !== index})
}

// var ColorMapping = {
//   0: "red",
//   1: "green",
//   2: "blue",
// };

// var SignMapping = {
//   -1: "minus",
//   0: "neutral",
//   1: "plus",
// };

// Alchemical -> Alchemical -> Potion
function mix(alchemicalA, alchemicalB) {
  var mean = _.zipWith(alchemicalA, alchemicalB, function(a,b){return (a+b)/2})
  for (var index = 0; index < 3; index++) {
    if (mean[(index+1) % 3] !== 0)
      mean[index] = 0
  }

  return mean
}

// http://stackoverflow.com/a/20871714/6036628
function permutator(inputArr) {
  var results = [];

  function permute(arr, memo) {
    var cur, memo = memo || [];

    for (var i = 0; i < arr.length; i++) {
      cur = arr.splice(i, 1);
      if (arr.length === 0) {
        results.push(memo.concat(cur));
      }
      permute(arr.slice(), memo.concat(cur));
      arr.splice(i, 0, cur[0]);
    }

    return results;
  }

  return permute(inputArr);
}

class Fact {
  constructor() {}
  check(world) {return "hi"} // Stub
}

class TwoIngredientFact extends Fact {
  constructor(ingredients, possibleResults) {
    super()
    this.ingredients = ingredients
    this.possibleResults = possibleResults
  }

  check(world) {
    var alchemicalA = world[this.ingredients[0]]
    var alchemicalB = world[this.ingredients[1]]
    var result = mix(alchemicalA, alchemicalB)
    var potionIndex = _.findIndex(_.values(potions), _.curry(_.isEqual)(result))
    return this.possibleResults[potionIndex]
  }
}

// This is what a set of aspects looks like:
// [[1,0,0], [0,0,-1]] is red+ or blue-
class OneIngredientFact extends Fact {
  constructor(ingredient, setOfAspects) {
    super()
    this.ingredient = ingredient
    this.setOfAspects = setOfAspects
  }

  check(world) {
    var alchemical = world[fact.ingredient]
    for (var aspectIndex = 0; aspectIndex < this.setOfAspects.length; aspectIndex++) {
      var aspect = this.setOfAspects[aspectIndex]
      for (var i = 0; i < 3; i++) {
        if (aspect[i] === alchemical[i]) {
          return true;
        }
      }
    }
    return false;
  }
}

// ===== Unit tests =====

// Here is what a World looks like:
var exampleWorld = [
  [+1, -1, -1],
  [-1, +1, -1],
  [+1, -1, +1],
  [-1, +1, +1],
  [+1, +1, +1],
  [-1, -1, +1],
  [+1, +1, -1],
  [-1, -1, -1],
]

// This is what an Ingredient looks like:
var exampleIngredientA = 4
var exampleIngredientB = 5

// This is what a Potion looks like:
var examplePotionA = [0, 0, 1] // this is a blue+
var examplePotionB = [0, 0, 0] // this is soup

// This is what a Fact looks like:
// {
//   ingredients: [5, 7]
//   possibleResults: [true, false, false, false, false, false, true]
// }
// combining ingredient 5 and 7 make either a red+ or a soup
var exampleFact = new Fact(
  [exampleIngredientA, exampleIngredientB],
  [examplePotionA, examplePotionB]
)

console.log(exampleFact.check(exampleWorld))

export default AlchemistsSolverApp;
<commit_msg>fix onTouchTap bug<commit_after>// === UI stuff ===
//TODO visualize the alchemicals in the row headers of the table
//TODO add pictures for the ingredients in the column headers of the table
//TODO add pictures for the ingredients in the radio buttons
//TODO add pictures of the potions for the check boxes
//TODO add refresh indicators http://www.material-ui.com/#/components/refresh-indicator
//TODO downsize columns (and rows)

// === Additional views ===
//TODO add view to see individual worlds when you have just a few

// === Other stuff ===
//TODO add other fact types (periscope / beginner debunking
//TODO make the view match the state for the buttons etc.
//TODO fix lag issues

// The expected number of bits of information from real science is
// -[1/7*lg(1/7) * 7]
// = 2.807 bits

// The expected number of bits of information from experimenting randomly on an adventurer is
// -[1/7*lg(1/7) + 2/7*lg(2/7) + 1/7*lg(1/7) + 3/7*lg(3/7)]
// = 1.842 bits

import React from 'react';
// import logo from './logo.svg';
import './App.css';

import _ from 'lodash'
import update from 'react-addons-update'
import PureRenderMixin from 'react-addons-pure-render-mixin';

import {Table, TableBody, TableHeader, TableHeaderColumn, TableRow, TableRowColumn} from 'material-ui/Table';
import RaisedButton from 'material-ui/RaisedButton';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';

import injectTapEventPlugin from 'react-tap-event-plugin';
injectTapEventPlugin();

const style = {
  margin: 12,
};

// This is what an Alchemical looks like:
// [1, 1, -1]

// Here are all of the Alchemicals:
var alchemicals = [
  [-1, +1, -1],
  [+1, -1, +1],
  [+1, -1, -1],
  [-1, +1, +1],
  [-1, -1, +1],
  [+1, +1, -1],
  [-1, -1, -1],
  [+1, +1, +1],
]

var ingredients = [
  "Mushroom",
  "Fern",
  "Toad",
  "Birdclaw",
  "Flower",
  "Mandrake",
  "Scorpion",
  "Feather",
]

var potions = {
  "Red+":   [+1, 0, 0],
  "Red-":   [-1, 0, 0],
  "Green+": [0, +1, 0],
  "Green-": [0, -1, 0],
  "Blue+":  [0, 0, +1],
  "Blue-":  [0, 0, -1],
  "Soup":   [0, 0, 0],
}

var ReactFact = React.createClass({
  mixins: [PureRenderMixin],

  render: function() {
    return

      {this.props.index}: {JSON.stringify(this.props.item)}
      <RaisedButton onClick={this.props.deleteFact} value={this.props.index} style={style}>Delete</RaisedButton>

  }
});

var FactList = React.createClass({
  mixins: [PureRenderMixin],

  render: function() {
    return
{this.props.items.map((fact, factIndex) => <ReactFact key={factIndex} item={fact} deleteFact={_.bind(function(){this.props.deleteFact(factIndex)}, this)}/>)}
;
  }
});

var Ingredient = React.createClass({
  mixins: [PureRenderMixin],

  render: function() {
    return
<input type="radio" name="ingredient" onChange={this.props.callback}/>{this.props.name}

  }
})

var Potion = React.createClass({
  mixins: [PureRenderMixin],

  render: function() {
    return
<input type="checkbox" name="potion" onChange={this.props.callback}/>{this.props.name}

  }
})

var SheetCell = React.createClass({
  mixins: [PureRenderMixin],

  render: function() {
    return <TableRowColumn>{this.percentage()}</TableRowColumn>
  },
  percentage: function() {
    var worlds = _.filter(this.props.worlds, _.bind(function(world) {
      return _.isEqual(world[this.props.ingredientIndex], this.props.alchemical)
    }, this))
    return Math.round(100 * worlds.length / this.props.worlds.length, 0)
  }
})

var SheetRow = React.createClass({
  mixins: [PureRenderMixin],

  render: function() {
    return (
      <TableRow>
        <TableRowColumn>{this.props.alchemical}</TableRowColumn>
        {ingredients.map((ingredient, index) => <SheetCell ingredientIndex={index} alchemical={this.props.alchemical} key={index} worlds={this.props.worlds}/>)}
      </TableRow>
    )
  }
})

var AlchemistsSolverApp = React.createClass({
  mixins: [PureRenderMixin],

  getInitialState: function() {
    return {
      factlist: [],
      currentFact: this.makeNewFact(),
    }
  },
  makeNewFact: function() {
    return new TwoIngredientFact([0, 1], [false, false, false, false, false, false, false])
  },
  handleSubmit: function(e) {
    e.preventDefault();
    this.setState({
      factlist: this.state.factlist.concat([this.state.currentFact]),
    });
  },
  ingredientChange: function(ingredientIndex, ingredient) {
    var newIngredients = [];
    newIngredients[ingredientIndex] = ingredient;
    var newState = update(this.state,
      {currentFact: {ingredients: {$merge: newIngredients}}})
    this.setState(newState)
  },
  potionChange: function(potionIndex) {
    var newPossibleResults = [];
    newPossibleResults[potionIndex] = !this.state.currentFact.possibleResults[potionIndex];
    var newState = update(this.state,
      {currentFact: {possibleResults: {$merge: newPossibleResults}}})
    this.setState(newState)
  },
  render: function() {
    var self = this;

    var worlds = permutator(alchemicals)
    for (var factIndex in this.state.factlist) {
      var fact = this.state.factlist[factIndex]
      worlds = _.filter(worlds, _.bind(fact.check, fact))
    }

    return (
      <MuiThemeProvider>

Alchemists Solver

          <FactList items={this.state.factlist} deleteFact={this.deleteFact}/>

          <form action="" style={{display: "inline-block"}}>
            {ingredients.map((name, index) => <Ingredient name={name} ingredientNumber={0} key={index} callback={function() {self.ingredientChange(0, index)}} />)}
          </form>

          <form action="" style={{display: "inline-block"}}>
            {ingredients.map((name, index) => <Ingredient name={name} ingredientNumber={1} key={index} callback={function() {self.ingredientChange(1, index)}} />)}
          </form>

          <form action="" style={{display: "inline-block"}}>
            {_.keys(potions).map((name, index) => <Potion name={name} key={index} callback={function() {self.potionChange(index)}} />)}
          </form>

          <RaisedButton label="Add Fact" primary={true} style={style} onClick={this.handleSubmit}/>

Remaining worlds: {worlds.length}

            <TableHeader displaySelectAll={false} adjustForCheckbox={false}>
              <TableRow>
                <TableHeaderColumn/>
                {ingredients.map((name, index) => <TableHeaderColumn key={index}>{name}</TableHeaderColumn>)}
              </TableRow>
            </TableHeader>
            <TableBody>
              {alchemicals.map((alchemical, index) => <SheetRow alchemical={alchemical} key={index} worlds={worlds}/>)}
            </TableBody>

      </MuiThemeProvider>
    );
  },
  deleteFact: function(deleteIndex) {
    this.setState({factlist: removeAtIndex(this.state.factlist, deleteIndex)})
  },
});

// non-mutating
function removeAtIndex(arr, index) {
  return _.filter(arr, function(val, idx) {return idx !== index})
}

// var ColorMapping = {
//   0: "red",
//   1: "green",
//   2: "blue",
// };

// var SignMapping = {
//   -1: "minus",
//   0: "neutral",
//   1: "plus",
// };

// Alchemical -> Alchemical -> Potion
function mix(alchemicalA, alchemicalB) {
  var mean = _.zipWith(alchemicalA, alchemicalB, function(a,b){return (a+b)/2})
  for (var index = 0; index < 3; index++) {
    if (mean[(index+1) % 3] !== 0)
      mean[index] = 0
  }

  return mean
}

// http://stackoverflow.com/a/20871714/6036628
function permutator(inputArr) {
  var results = [];

  function permute(arr, memo) {
    var cur, memo = memo || [];

    for (var i = 0; i < arr.length; i++) {
      cur = arr.splice(i, 1);
      if (arr.length === 0) {
        results.push(memo.concat(cur));
      }
      permute(arr.slice(), memo.concat(cur));
      arr.splice(i, 0, cur[0]);
    }

    return results;
  }

  return permute(inputArr);
}

class Fact {
  constructor() {}
  check(world) {return "hi"} // Stub
}

class TwoIngredientFact extends Fact {
  constructor(ingredients, possibleResults) {
    super()
    this.ingredients = ingredients
    this.possibleResults = possibleResults
  }

  check(world) {
    var alchemicalA = world[this.ingredients[0]]
    var alchemicalB = world[this.ingredients[1]]
    var result = mix(alchemicalA, alchemicalB)
    var potionIndex = _.findIndex(_.values(potions), _.curry(_.isEqual)(result))
    return this.possibleResults[potionIndex]
  }
}

// This is what a set of aspects looks like:
// [[1,0,0], [0,0,-1]] is red+ or blue-
class OneIngredientFact extends Fact {
  constructor(ingredient, setOfAspects) {
    super()
    this.ingredient = ingredient
    this.setOfAspects = setOfAspects
  }

  check(world) {
    var alchemical = world[fact.ingredient]
    for (var aspectIndex = 0; aspectIndex < this.setOfAspects.length; aspectIndex++) {
      var aspect = this.setOfAspects[aspectIndex]
      for (var i = 0; i < 3; i++) {
        if (aspect[i] === alchemical[i]) {
          return true;
        }
      }
    }
    return false;
  }
}

// ===== Unit tests =====

// Here is what a World looks like:
var exampleWorld = [
  [+1, -1, -1],
  [-1, +1, -1],
  [+1, -1, +1],
  [-1, +1, +1],
  [+1, +1, +1],
  [-1, -1, +1],
  [+1, +1, -1],
  [-1, -1, -1],
]

// This is what an Ingredient looks like:
var exampleIngredientA = 4
var exampleIngredientB = 5

// This is what a Potion looks like:
var examplePotionA = [0, 0, 1] // this is a blue+
var examplePotionB = [0, 0, 0] // this is soup

// This is what a Fact looks like:
// {
//   ingredients: [5, 7]
//   possibleResults: [true, false, false, false, false, false, true]
// }
// combining ingredient 5 and 7 make either a red+ or a soup
var exampleFact = new Fact(
  [exampleIngredientA, exampleIngredientB],
  [examplePotionA, examplePotionB]
)

console.log(exampleFact.check(exampleWorld))

export default AlchemistsSolverApp;
<|endoftext|>


<commit_before>//META{"name":"staticAvatars"}*//

/*@cc_on
@if (@_jscript)
    // Offer to self-install for clueless users that try to run this directly.
    var shell = WScript.CreateObject("WScript.Shell");
    var fs = new ActiveXObject("Scripting.FileSystemObject");
    var pathPlugins = shell.ExpandEnvironmentStrings("%APPDATA%\\BetterDiscord\\plugins");
    var pathSelf = WScript.ScriptFullName;
    // Put the user at ease by addressing them in the first person
    shell.Popup("It looks like you mistakenly tried to run me directly. (don't do that!)", 0, "I'm a plugin for BetterDiscord", 0x30);
    if (fs.GetParentFolderName(pathSelf) === fs.GetAbsolutePathName(pathPlugins)) {
        shell.Popup("I'm in the correct folder already.\nJust reload Discord with Ctrl+R.", 0, "I'm already installed", 0x40);
    } else if (!fs.FolderExists(pathPlugins)) {
        shell.Popup("I can't find the BetterDiscord plugins folder.\nAre you sure it's even installed?", 0, "Can't install myself", 0x10);
    } else if (shell.Popup("Should I copy myself to BetterDiscord's plugins folder for you?", 0, "Do you need some help?", 0x34) === 6) {
        fs.CopyFile(pathSelf, fs.BuildPath(pathPlugins, fs.GetFileName(pathSelf)), true);
        // Show the user where to put plugins in the future
        shell.Exec("explorer " + pathPlugins);
        shell.Popup("I'm installed!\nJust reload Discord with Ctrl+R.", 0, "Successfully installed", 0x40);
    }
    WScript.Quit();
@else @*/

var staticAvatars = function () {};

(function () {
    "use strict";

    staticAvatars.prototype.start = function () {
        const module = WebpackModules.findByUniqueProperties(["hasAnimatedAvatar", "default"], {cacheOnly: true});
        if (!module) {
            console.error("StaticAvatars:", "unable to monkey patch hasAnimatedAvatar method");
            return;
        }
        this._cancel = monkeyPatch(module, "hasAnimatedAvatar", {instead: ()=>false});
    };

    staticAvatars.prototype.stop = function () {
        if (this._cancel) {
            this._cancel();
            delete this._cancel;
        }
    };

    staticAvatars.prototype.load = function () {};

    staticAvatars.prototype.unload = function () {};

    staticAvatars.prototype.getName = function () {
        return "Static Avatars";
    };

    staticAvatars.prototype.getDescription = function () {
        return "Don't animate avatars in the chat area";
    };

    staticAvatars.prototype.getVersion = function () {
        return "2.0.0";
    };

    staticAvatars.prototype.getAuthor = function () {
        return "noodlebox";
    };

    /**
     * Function with no arguments and no return value that may be called to reverse changes that is done by {@link monkeyPatch} method, restoring (unpatching) original method.
     * @callback cancelPatch
     */

    /**
     * This is a shortcut for calling original method using this and arguments from data object. This is a function without input arguments. This function is defined as `() => data.returnValue = data.originalMethod.apply(data.thisObject, data.methodArguments)`
     * @callback originalMethodCall
     * @return {*} The same value, which is returned from original method, also this value would be written into `data.returnValue`
     */

    /**
     * A callback that modifies method logic. Callback is called on each call of original method and have all data about original call. Any of the data can be modified if you need, but do it wisely.
     * @callback doPatchCallback
     * @param {PatchData} data Data object with all information about current that you may need in your patching callback.callback.
     * @return {*} Makes sense only when used as `instead` parameter in {@link monkeyPatch}. If returned something other then undefined - it replaces value in `returnValue` param. If used as `before` or `after` parameters - return value if ignored.
     */

    /**
     * This is function for monkey-patching any object method. Can make patch before, after or instead of target method.
     * Be careful when monkey-patching. Think not only about original functionality of target method and you changes, but also about develovers of other plugins, who may also patch this method before or after you. Try to change target method behaviour little as you can, and try to never change method signatures.
     * By default this function makes log messages about each patching and unpatching, so you and other developers can see what methods a patched. This messages may be suppressed.
     * Display name of patched method is changed, so you can see if function is patched and how many times while debuging or in the stack trace. Also patched function have property `__monkeyPatched` is set to true, in case you want to check something programmatically.
     *
     * @param {object} what Object to be patched. You can can also pass class prototypes to patch all class instances. If you are patching prototype of react component you may also need {@link Renderer.rebindMethods}.
     * @param {string} methodName The name of the target message to be patched.
     * @param {object} options Options object. You should provide at least one of `before`, `after` or `instead` parameters. Other parameters are optional.
     * @param {doPatchCallback} options.before Callback that will be called before original target method call. You can modify arguments here, so it will be passed to original method. Can be combined with `after`.
     * @param {doPatchCallback} options.after Callback that will be called after original target method call. You can modify return value here, so it will be passed to external code which calls target method. Can be combined with `before`.
     * @param {doPatchCallback} options.instead Callback that will be called instead of original target method call. You can get access to original method using `originalMethod` parameter if you want to call it, but you do not have to. Can't be combined with `before` and `after`.
     * @param {boolean} [options.once=false] Set to true if you want automatically unpatch method after first call.
     * @param {boolean} [options.silent=false] Set to true if you want to suppress log messages about patching and unpatching. Useful to avoid clogging the console in case of frequent conditional patching/unpatching, for example from another monkeyPatch callback.
     * @param {boolean} [options.displayName] You can provide meaningful name of class/object provided in `what` param for logging purposes. By default there will be a try to determine name automatically.
     * @return {cancelPatch} Function with no arguments and no return value that should be called to cancel (unpatch) this patch. You should save and run it when your plugin is stopped.
     */
    const monkeyPatch = (what, methodName, options) => {
        const {before, after, instead, once = false, silent = false} = options;
        const displayName = options.displayName || what.displayName || what.name || what.constructor.displayName || what.constructor.name;
        if (!silent) console.log('patch', methodName, 'of', displayName);
        const origMethod = what[methodName];
        const cancel = () => {
            if (!silent) console.log('unpatch', methodName, 'of', displayName);
            what[methodName] = origMethod;
        };
        what[methodName] = function() {
            /**
             * @interface
             * @name PatchData
             * @property {object} thisObject Original `this` value in current call of patched method.
             * @property {Arguments} methodArguments Original `arguments` object in current call of patched method. Please, never change function signatures, as it may cause a lot of problems in future.
             * @property {cancelPatch} cancelPatch Function with no arguments and no return value that may be called to reverse patching of current method. Calling this function prevents running of this callback on further original method calls.
             * @property {function} originalMethod Reference to the original method that is patched. You can use in if you need some special usage. You should explicitly provide this value and method arguments when you call this function.
             * @property {originalMethodCall} callOriginalMethod This is a shortcut for calling original method using this and arguments from data object.
             * @property {*} returnValue This is a value returned from original function call. This property is avilable only in `after` callback, or in `instead` callback after calling `callOriginalMethod` function
             */
            const data = {
                thisObject: this,
                methodArguments: arguments,
                cancelPatch: cancel,
                originalMethod: origMethod,
                callOriginalMethod: () => data.returnValue = data.originalMethod.apply(data.thisObject, data.methodArguments)
            };
            if (instead) {
                const tempRet = instead(data);
                if (tempRet !== undefined)
                    data.returnValue = tempRet;
            }
            else {
                if (before) before(data);
                data.callOriginalMethod();
                if (after) after(data);
            }
            if (once) cancel();
            return data.returnValue;
        };
        what[methodName].__monkeyPatched = true;
        what[methodName].displayName = 'patched ' + (what[methodName].displayName || methodName);
        return cancel;
    };

    const WebpackModules = (() => {

        const req = webpackJsonp([], {
            '__extra_id__': (module, exports, req) => exports.default = req
        }, ['__extra_id__']).default;
        delete req.m['__extra_id__'];
        delete req.c['__extra_id__'];

        /**
         * Predicate for searching module
         * @callback modulePredicate
         * @param {*} module Module to test
         * @return {boolean} Thue if it is module that you need
         */

        /**
         * Look through all modules of internal Discord's Webpack and return first one that match filter predicate.
         * At first this function will look thruogh alreary loaded modules cache. If no one of loaded modules is matched - then this function tries to load all modules and match for them. Loading any module may have unexpected side effects, like changing current locale of moment.js, so in that case there will be a warning the console. If no module matches - function will return null. You sould always take such predicate to match something, gut your code should be ready to recieve null in case if Discord update something in codebase.
         * If module is ES6 module and has default property, consider default first, otherwise - full module object.
         * @param {modulePredicate} filter Predicate to match module
         * @param {object} [options] Options object.
         * @param {boolean} [options.cacheOnly=false] Set to true if you want to search only the cache for modules.
         * @return {*} First module that matched by filter or null if none is matched.
         */
        const find = (filter, options = {}) => {
            const {cacheOnly = false} = options;
            for (let i in req.c) {
                if (req.c.hasOwnProperty(i)) {
                    let m = req.c[i].exports;
                    if (m && m.__esModule && m.default && filter(m.default))
                        return m.default;
                    if (m && filter(m))
                        return m;
                }
            }
            if (cacheOnly) {
                console.warn('Cannot find loaded module in cache');
                return null;
            }
            console.warn('Cannot find loaded module in cache. Loading all modules may have unexpected side effects');
            for (let i = 0; i < req.m.length; ++i) {
                let m = req(i);
                if (m && m.__esModule && m.default && filter(m.default))
                    return m.default;
                if (m && filter(m))
                    return m;
            }
            console.warn('Cannot find module');
            return null;
        };

        /**
         * Look through all modules of internal Discord's Webpack and return first object that has all of following properties. You should be ready that in any moment, after Discord update, this function may start returning null (if no such object exists any more) or even some different object with the same properties. So you should provide all property names that you use, and often even some extra properties to make sure you'll get exactly what you want.
         * @see Read {@link find} documentation for more details how search works
         * @param {string[]} propNames Array of property names to look for
         * @param {object} [options] Options object to pass to {@link find}.
         * @return {object} First module that matched by propNames or null if none is matched.
         */
        const findByUniqueProperties = (propNames, options) => find(module => propNames.every(prop => module[prop] !== undefined), options);

        /**
         * Look through all modules of internal Discord's Webpack and return first object that has displayName property with following value. This is useful for searching React components by name. Take into account that not all components are exported as modules. Also there might be several components with same names
         * @see Use {@link ReactComponents} as another way to get react components
         * @see Read {@link find} documentation for more details how search works
         * @param {string} displayName Display name property value to look for
         * @param {object} [options] Options object to pass to {@link find}.
         * @return {object} First module that matched by displayName or null if none is matched.
         */
        const findByDisplayName = (displayName, options) => find(module => module.displayName === displayName, options);

        return {find, findByUniqueProperties, findByDisplayName};

    })();
})();

/*@end @*/
<commit_msg>Use Discord Internals library with StaticAvatars if at least version 1.4<commit_after>//META{"name":"staticAvatars"}*//

/*@cc_on
@if (@_jscript)
    // Offer to self-install for clueless users that try to run this directly.
    var shell = WScript.CreateObject("WScript.Shell");
    var fs = new ActiveXObject("Scripting.FileSystemObject");
    var pathPlugins = shell.ExpandEnvironmentStrings("%APPDATA%\\BetterDiscord\\plugins");
    var pathSelf = WScript.ScriptFullName;
    // Put the user at ease by addressing them in the first person
    shell.Popup("It looks like you mistakenly tried to run me directly. (don't do that!)", 0, "I'm a plugin for BetterDiscord", 0x30);
    if (fs.GetParentFolderName(pathSelf) === fs.GetAbsolutePathName(pathPlugins)) {
        shell.Popup("I'm in the correct folder already.\nJust reload Discord with Ctrl+R.", 0, "I'm already installed", 0x40);
    } else if (!fs.FolderExists(pathPlugins)) {
        shell.Popup("I can't find the BetterDiscord plugins folder.\nAre you sure it's even installed?", 0, "Can't install myself", 0x10);
    } else if (shell.Popup("Should I copy myself to BetterDiscord's plugins folder for you?", 0, "Do you need some help?", 0x34) === 6) {
        fs.CopyFile(pathSelf, fs.BuildPath(pathPlugins, fs.GetFileName(pathSelf)), true);
        // Show the user where to put plugins in the future
        shell.Exec("explorer " + pathPlugins);
        shell.Popup("I'm installed!\nJust reload Discord with Ctrl+R.", 0, "Successfully installed", 0x40);
    }
    WScript.Quit();
@else @*/

var staticAvatars = function () {};

(function () {
    "use strict";

    staticAvatars.prototype.start = function () {
        const module = WebpackModules.findByUniqueProperties(["hasAnimatedAvatar", "default"], {cacheOnly: true});
        if (!module) {
            console.error("StaticAvatars:", "unable to monkey patch hasAnimatedAvatar method");
            return;
        }
        this._cancel = monkeyPatch(module, "hasAnimatedAvatar", {instead: ()=>false});
    };

    staticAvatars.prototype.stop = function () {
        if (this._cancel) {
            this._cancel();
            delete this._cancel;
        }
    };

    staticAvatars.prototype.load = function () {};

    staticAvatars.prototype.unload = function () {};

    staticAvatars.prototype.getName = function () {
        return "Static Avatars";
    };

    staticAvatars.prototype.getDescription = function () {
        return "Don't animate avatars in the chat area";
    };

    staticAvatars.prototype.getVersion = function () {
        return "2.0.0";
    };

    staticAvatars.prototype.getAuthor = function () {
        return "noodlebox";
    };

    // If samogot's DiscordInternals lib exists, use it. Otherwise, fall back on bundled code below.
    // See: https://github.com/samogot/betterdiscord-plugins/tree/master/v2/1Lib%20Discord%20Internals
    const DI = window.DiscordInternals;
    const hasLib = !!(DI && DI.versionCompare && DI.versionCompare(DI.version || "", "1.4") >= 0);

    /**
     * Function with no arguments and no return value that may be called to revert changes made by {@link monkeyPatch} method, restoring (unpatching) original method.
     * @callback cancelPatch
     */

    /**
     * This is a shortcut for calling original method using `this` and `arguments` from original call. This function accepts no arguments. This function is defined as `() => data.returnValue = data.originalMethod.apply(data.thisObject, data.methodArguments)`
     * @callback originalMethodCall
     * @return {*} The same value, which is returned from original method, also this value would be written into `data.returnValue`
     */

    /**
     * A callback that modifies method logic. This callback is called on each call of the original method and is provided all data about original call. Any of the data can be modified if necessary, but do so wisely.
     * @callback doPatchCallback
     * @param {PatchData} data Data object with information about current call and original method that you may need in your patching callback.
     * @return {*} Makes sense only when used as `instead` parameter in {@link monkeyPatch}. If something other than `undefined` is returned, the returned value replaces the value of `data.returnValue`. If used as `before` or `after` parameters, return value is ignored.
     */

    /**
     * This function monkey-patches a method on an object. The patching callback may be run before, after or instead of target method.
     * Be careful when monkey-patching. Think not only about original functionality of target method and your changes, but also about developers of other plugins, who may also patch this method before or after you. Try to change target method behaviour as little as possible, and avoid changing method signatures.
     * By default, this function logs to the console whenever a method is patched or unpatched in order to aid debugging by you and other developers, but these messages may be suppressed with the `silent` option.
     * Display name of patched method is changed, so you can see if a function has been patched (and how many times) while debugging or in the stack trace. Also, patched methods have property `__monkeyPatched` set to `true`, in case you want to check something programmatically.
     *
     * @author samogot
     * @param {object} what Object to be patched. You can can also pass class prototypes to patch all class instances. If you are patching prototype of react component you may also need {@link Renderer.rebindMethods}.
     * @param {string} methodName The name of the target message to be patched.
     * @param {object} options Options object. You should provide at least one of `before`, `after` or `instead` parameters. Other parameters are optional.
     * @param {doPatchCallback} options.before Callback that will be called before original target method call. You can modify arguments here, so it will be passed to original method. Can be combined with `after`.
     * @param {doPatchCallback} options.after Callback that will be called after original target method call. You can modify return value here, so it will be passed to external code which calls target method. Can be combined with `before`.
     * @param {doPatchCallback} options.instead Callback that will be called instead of original target method call. You can get access to original method using `originalMethod` parameter if you want to call it, but you do not have to. Can't be combined with `before` and `after`.
     * @param {boolean} [options.once=false] Set to `true` if you want to automatically unpatch method after first call.
     * @param {boolean} [options.silent=false] Set to `true` if you want to suppress log messages about patching and unpatching. Useful to avoid clogging the console in case of frequent conditional patching/unpatching, for example from another monkeyPatch callback.
     * @param {string} [options.displayName] You can provide meaningful name for class/object provided in `what` param for logging purposes. By default, this function will try to determine name automatically.
     * @return {cancelPatch} Function with no arguments and no return value that should be called to cancel (unpatch) this patch. You should save and run it when your plugin is stopped.
     */
    const monkeyPatch = hasLib && DI.monkeyPatch || ((what, methodName, options) => {
        const {before, after, instead, once = false, silent = false} = options;
        const displayName = options.displayName || what.displayName || what.name || what.constructor.displayName || what.constructor.name;
        if (!silent) console.log('patch', methodName, 'of', displayName);
        const origMethod = what[methodName];
        const cancel = () => {
            if (!silent) console.log('unpatch', methodName, 'of', displayName);
            what[methodName] = origMethod;
        };
        what[methodName] = function() {
            /**
             * @interface
             * @name PatchData
             * @property {object} thisObject Original `this` value in current call of patched method.
             * @property {Arguments} methodArguments Original `arguments` object in current call of patched method. Please, never change function signatures, as it may cause a lot of problems in future.
             * @property {cancelPatch} cancelPatch Function with no arguments and no return value that may be called to reverse patching of current method. Calling this function prevents running of this callback on further original method calls.
             * @property {function} originalMethod Reference to the original method that is patched. You can use it if you need some special usage. You should explicitly provide a value for `this` and any method arguments when you call this function.
             * @property {originalMethodCall} callOriginalMethod This is a shortcut for calling original method using `this` and `arguments` from original call.
             * @property {*} returnValue This is a value returned from original function call. This property is available only in `after` callback or in `instead` callback after calling `callOriginalMethod` function.
             */
            const data = {
                thisObject: this,
                methodArguments: arguments,
                cancelPatch: cancel,
                originalMethod: origMethod,
                callOriginalMethod: () => data.returnValue = data.originalMethod.apply(data.thisObject, data.methodArguments)
            };
            if (instead) {
                const tempRet = instead(data);
                if (tempRet !== undefined)
                    data.returnValue = tempRet;
            }
            else {
                if (before) before(data);
                data.callOriginalMethod();
                if (after) after(data);
            }
            if (once) cancel();
            return data.returnValue;
        };
        what[methodName].__monkeyPatched = true;
        what[methodName].displayName = 'patched ' + (what[methodName].displayName || methodName);
        return cancel;
    });

    /**
     * @author samogot
     */
    const WebpackModules = hasLib && DI.WebpackModules || (() => {

        const req = webpackJsonp([], {
            '__extra_id__': (module, exports, req) => exports.default = req
        }, ['__extra_id__']).default;
        delete req.m['__extra_id__'];
        delete req.c['__extra_id__'];

        /**
         * Predicate for searching module
         * @callback modulePredicate
         * @param {*} module Module to test
         * @return {boolean} Returns `true` if `module` matches predicate.
         */

        /**
         * Look through all modules of internal Discord's Webpack and return first one that matches filter predicate.
         * At first this function will look through already loaded modules cache. If no loaded modules match, then this function tries to load all modules and match for them. Loading any module may have unexpected side effects, like changing current locale of moment.js, so in that case there will be a warning the console. If no module matches, this function returns `null`. You should always try to provide a predicate that will match something, but your code should be ready to receive `null` in case of changes in Discord's codebase.
         * If module is ES6 module and has default property, consider default first; otherwise, consider the full module object.
         * @param {modulePredicate} filter Predicate to match module
         * @param {object} [options] Options object.
         * @param {boolean} [options.cacheOnly=false] Set to `true` if you want to search only the cache for modules.
         * @return {*} First module that matches `filter` or `null` if none match.
         */
        const find = (filter, options = {}) => {
            const {cacheOnly = false} = options;
            for (let i in req.c) {
                if (req.c.hasOwnProperty(i)) {
                    let m = req.c[i].exports;
                    if (m && m.__esModule && m.default && filter(m.default))
                        return m.default;
                    if (m && filter(m))
                        return m;
                }
            }
            if (cacheOnly) {
                console.warn('Cannot find loaded module in cache');
                return null;
            }
            console.warn('Cannot find loaded module in cache. Loading all modules may have unexpected side effects');
            for (let i = 0; i < req.m.length; ++i) {
                let m = req(i);
                if (m && m.__esModule && m.default && filter(m.default))
                    return m.default;
                if (m && filter(m))
                    return m;
            }
            console.warn('Cannot find module');
            return null;
        };

        /**
         * Look through all modules of internal Discord's Webpack and return first object that has all of following properties. You should be ready that in any moment, after Discord update, this function may start returning `null` (if no such object exists anymore) or even some different object with the same properties. So you should provide all property names that you use, and often even some extra properties to make sure you'll get exactly what you want.
         * @see Read {@link find} documentation for more details how search works
         * @param {string[]} propNames Array of property names to look for
         * @param {object} [options] Options object to pass to {@link find}.
         * @return {object} First module that matches `propNames` or `null` if none match.
         */
        const findByUniqueProperties = (propNames, options) => find(module => propNames.every(prop => module[prop] !== undefined), options);

        /**
         * Look through all modules of internal Discord's Webpack and return first object that has `displayName` property with following value. This is useful for searching for React components by name. Take into account that not all components are exported as modules. Also, there might be several components with the same name.
         * @see Use {@link ReactComponents} as another way to get react components
         * @see Read {@link find} documentation for more details how search works
         * @param {string} displayName Display name property value to look for
         * @param {object} [options] Options object to pass to {@link find}.
         * @return {object} First module that matches `displayName` or `null` if none match.
         */
        const findByDisplayName = (displayName, options) => find(module => module.displayName === displayName, options);

        return {find, findByUniqueProperties, findByDisplayName};

    })();
})();

/*@end @*/
<|endoftext|>


<commit_before>const emojiReplacer = (function(){

	const wrappers = {
		'parentheses': ['(', ')'],
		'squarebrackets': ['[', ']'],
		'colons': [':', ':'],
	}

	const settings = {};

	settings.emojiDisplay = 'emoji';
	settings.showTranslation = true;
	settings.wrapper = 'parentheses';
	settings.wrapStart = '';
	settings.wrapEnd = '';
	settings.ignoreFlags = true;

	const pattern = buildPattern();
	const template = document.createElement('template');

	return {
		settings,
		set,
		translateTextNode: buildTranslatedNodes
	}

	function buildPattern() {
		const emojis = Object.keys(namesDictionary);
		// sort by longest first to grep longest match
		emojis.sort((a,b) => {
			return b.length - a.length;
		});

		const escapedEmojis = emojis.map(emoji => escapeForRegex(emoji));
		return new RegExp(escapedEmojis.join('|'), 'ug');
	}

	function getReplacedEmoji(emoji, translation) {
		let replacement = emoji;
		switch (settings.emojiDisplay) {
			case 'hide':
				replacement = '';
				break;
			case 'tooltip':
				replacement = `<span title="${translation}">${emoji}</span>`;
				break;
		}
		return replacement;
	}

	function getAndVerifyMessage(message) {
		try {
			const localisation = browser.i18n.getMessage(message);
			if (localisation.length <= 0) {
				return;
			}
			return localisation;
		} catch(e) {
			return;
		}
	}

	function getLocalisedNameForEmoji(emoji) {
		let translation = getAndVerifyMessage(emoji);

		if (isUndefined(translation)) {
			// try trimming variation selectors
			translation = getAndVerifyMessage(trimVS(emoji));
		}

		if (isUndefined(translation)) {
			// fall back to non-localised name
			translation = namesDictionary[emoji];
		}

		return translation;
	}

	function getTranslationForEmoji(emoji) {
		const name = getLocalisedNameForEmoji(emoji);

		let userWrappers = ['', ''];
		if (settings.wrapper !== 'nothing') {
			userWrappers = wrappers[settings.wrapper];
			if (settings.wrapper === 'custom') {
				userWrappers = [settings.wrapStart, settings.wrapEnd];
			}
		}
		return `${userWrappers[0]}${name}${userWrappers[1]}`;
	}

	// returns list of translated emojis and list of surrounding texts
	// if no emojis are found, returns false
	function getReplacedParts(original) {
		if (pattern.test(original)) {
			const emojis = original.match(pattern);
			const nonemojis = original.split(pattern);

			// pad end of emojis and translations because split length should always be emojis length + 1
			const translations = emojis.map(emoji => getTranslationForEmoji(emoji)).concat('');
			const replaceEmojis = emojis.map((emoji, index) => getReplacedEmoji(emoji, translations[index])).concat('');

			if (nonemojis.length !== replaceEmojis.length || replaceEmojis.length !== translations.length) {
				console.warn('Programmer error: assumption of split length is incorrect.', nonemojis, emojis, translations);
			}

			return nonemojis.map((nonemoji, index) => {
				const replacedParts = {
					nonemoji: nonemoji,
					emoji: replaceEmojis[index],
					translation: translations[index]
				};

				// treat regional indicators as nonemojis
				if (settings.ignoreFlags && /[\u{1F1E6}-\u{1F1FF}]/u.test(emojis[index])) {
					replacedParts['nonemoji'] = nonemoji.concat(emojis[index]);
					replacedParts['emoji'] = '';
					replacedParts['translation'] = '';
				}

				// suppress translation after emoji
				if (!settings.showTranslation) {
					replacedParts['translation'] = '';
				}

				return replacedParts;
			});
		}
		return false;
	}

	function buildTranslatedNodes(originalNode) {
		const originalText = originalNode.nodeValue;
		const parent = originalNode.parentElement;
		const ZEROWIDTHJOINER = '\u200D';

		// do not replace script contents
		if (!parent || parent.tagName.toLowerCase() !== 'script') {

			// splice together translations and surrounding text
			const replacementDictionary = getReplacedParts(originalText);

			if (replacementDictionary) {
				let index = 0;
				while (index < replacementDictionary.length) {
					let {nonemoji, emoji, translation} = replacementDictionary[index];

					// collapse consecutive emoji matches in array
					while (++index < replacementDictionary.length) {
						// look ahead for empty sections in the original splitted by emojis
						const nextSection = replacementDictionary[index];

						const nonemojiIsEmpty = nextSection['nonemoji'] === '';
						const nonemojiIsZeroWidthJoiner = nextSection['nonemoji'] === ZEROWIDTHJOINER;

						if (nonemojiIsEmpty || nonemojiIsZeroWidthJoiner) {
							// add ZERO WIDTH JOINER back into emoji to display joined emojis properly
							if (nonemojiIsZeroWidthJoiner) {
								emoji = emoji.concat(ZEROWIDTHJOINER);
							}

							nonemoji = nonemoji.concat(nextSection['nonemoji']);
							emoji = emoji.concat(nextSection['emoji']);
							translation = translation.concat(nextSection['translation']);

						} else {
							break;
						}
					}

					// insert nodes for non-matched/surrounding text from original
					const node = document.createTextNode(nonemoji);
					parent.insertBefore(node, originalNode);

					// insert nodes for translated emojis
					// use `innerHTML` to support formatted html code
					template.innerHTML = `${emoji}${translation}`;

					parent.insertBefore(template.content, originalNode);
				}
				// clear original text node
				originalNode.nodeValue = "";
			}

		}
	}

	function escapeForRegex(sequence) {
		return sequence.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
	}

	// trim variation selectors surrounding sequence
	function trimVS(sequence) {
		const split = sequence.split(/^[\uFE00-\uFE0F]+|[\uFE00-\uFE0F]+$/);
		return split.filter(sequence => sequence.length>0)[0];
	}

	function isUndefined(variable) {
		return (typeof variable === 'undefined');
	}

	function set(setting, value) {
		settings[setting] = value;
	}
}());
<commit_msg>try translating emoji base and modifier separately if entire sequence is not in localisation<commit_after>const emojiReplacer = (function(){

	const wrappers = {
		'parentheses': ['(', ')'],
		'squarebrackets': ['[', ']'],
		'colons': [':', ':'],
	}

	const settings = {};

	settings.emojiDisplay = 'emoji';
	settings.showTranslation = true;
	settings.wrapper = 'parentheses';
	settings.wrapStart = '';
	settings.wrapEnd = '';
	settings.ignoreFlags = true;

	const pattern = buildPattern();
	const template = document.createElement('template');

	return {
		settings,
		set,
		translateTextNode: buildTranslatedNodes
	}

	function buildPattern() {
		const emojis = Object.keys(namesDictionary);
		// sort by longest first to grep longest match
		emojis.sort((a,b) => {
			return b.length - a.length;
		});

		const escapedEmojis = emojis.map(emoji => escapeForRegex(emoji));
		return new RegExp(escapedEmojis.join('|'), 'ug');
	}

	function getReplacedEmoji(emoji, translation) {
		let replacement = emoji;
		switch (settings.emojiDisplay) {
			case 'hide':
				replacement = '';
				break;
			case 'tooltip':
				replacement = `<span title="${translation}">${emoji}</span>`;
				break;
		}
		return replacement;
	}

	function getAndVerifyMessage(message) {
		try {
			const localisation = browser.i18n.getMessage(message);
			if (localisation.length <= 0) {
				return;
			}
			return localisation;
		} catch(e) {
			return;
		}
	}

	function generateLocalisationWithModifier(sequence) {
		const modifierPattern = /[\u{1F3FB}-\u{1F3FF}]/u;
		const modifierIndex = sequence.search(modifierPattern);

		if (modifierIndex >= 0) {
			const base = sequence.replace(modifierPattern, '');
			const modifier = modifierPattern.exec(sequence)[0];

			const translatedBase = getAndVerifyMessage(trimVS(base));
			const translatedModifier = getAndVerifyMessage(modifier);

			if (!isUndefined(translatedBase) &&
				!isUndefined(translatedModifier)) {
				return `${translatedBase}: ${translatedModifier}`;
			}
		}
	}

	function getLocalisedNameForEmoji(emoji) {
		let translation = getAndVerifyMessage(emoji);

		if (isUndefined(translation)) {
			// try trimming variation selectors
			translation = getAndVerifyMessage(trimVS(emoji));
		}

		if (isUndefined(translation)) {
			// try translating modifier and base separately
			translation = generateLocalisationWithModifier(trimVS(emoji));
		}

		if (isUndefined(translation)) {
			// fall back to non-localised name
			translation = namesDictionary[emoji];
		}

		return translation;
	}

	function getTranslationForEmoji(emoji) {
		const name = getLocalisedNameForEmoji(emoji);

		let userWrappers = ['', ''];
		if (settings.wrapper !== 'nothing') {
			userWrappers = wrappers[settings.wrapper];
			if (settings.wrapper === 'custom') {
				userWrappers = [settings.wrapStart, settings.wrapEnd];
			}
		}
		return `${userWrappers[0]}${name}${userWrappers[1]}`;
	}

	// returns list of translated emojis and list of surrounding texts
	// if no emojis are found, returns false
	function getReplacedParts(original) {
		if (pattern.test(original)) {
			const emojis = original.match(pattern);
			const nonemojis = original.split(pattern);

			// pad end of emojis and translations because split length should always be emojis length + 1
			const translations = emojis.map(emoji => getTranslationForEmoji(emoji)).concat('');
			const replaceEmojis = emojis.map((emoji, index) => getReplacedEmoji(emoji, translations[index])).concat('');

			if (nonemojis.length !== replaceEmojis.length || replaceEmojis.length !== translations.length) {
				console.warn('Programmer error: assumption of split length is incorrect.', nonemojis, emojis, translations);
			}

			return nonemojis.map((nonemoji, index) => {
				const replacedParts = {
					nonemoji: nonemoji,
					emoji: replaceEmojis[index],
					translation: translations[index]
				};

				// treat regional indicators as nonemojis
				if (settings.ignoreFlags && /[\u{1F1E6}-\u{1F1FF}]/u.test(emojis[index])) {
					replacedParts['nonemoji'] = nonemoji.concat(emojis[index]);
					replacedParts['emoji'] = '';
					replacedParts['translation'] = '';
				}

				// suppress translation after emoji
				if (!settings.showTranslation) {
					replacedParts['translation'] = '';
				}

				return replacedParts;
			});
		}
		return false;
	}

	function buildTranslatedNodes(originalNode) {
		const originalText = originalNode.nodeValue;
		const parent = originalNode.parentElement;
		const ZEROWIDTHJOINER = '\u200D';

		// do not replace script contents
		if (!parent || parent.tagName.toLowerCase() !== 'script') {

			// splice together translations and surrounding text
			const replacementDictionary = getReplacedParts(originalText);

			if (replacementDictionary) {
				let index = 0;
				while (index < replacementDictionary.length) {
					let {nonemoji, emoji, translation} = replacementDictionary[index];

					// collapse consecutive emoji matches in array
					while (++index < replacementDictionary.length) {
						// look ahead for empty sections in the original splitted by emojis
						const nextSection = replacementDictionary[index];

						const nonemojiIsEmpty = nextSection['nonemoji'] === '';
						const nonemojiIsZeroWidthJoiner = nextSection['nonemoji'] === ZEROWIDTHJOINER;

						if (nonemojiIsEmpty || nonemojiIsZeroWidthJoiner) {
							// add ZERO WIDTH JOINER back into emoji to display joined emojis properly
							if (nonemojiIsZeroWidthJoiner) {
								emoji = emoji.concat(ZEROWIDTHJOINER);
							}

							nonemoji = nonemoji.concat(nextSection['nonemoji']);
							emoji = emoji.concat(nextSection['emoji']);
							translation = translation.concat(nextSection['translation']);

						} else {
							break;
						}
					}

					// insert nodes for non-matched/surrounding text from original
					const node = document.createTextNode(nonemoji);
					parent.insertBefore(node, originalNode);

					// insert nodes for translated emojis
					// use `innerHTML` to support formatted html code
					template.innerHTML = `${emoji}${translation}`;

					parent.insertBefore(template.content, originalNode);
				}
				// clear original text node
				originalNode.nodeValue = "";
			}

		}
	}

	function escapeForRegex(sequence) {
		return sequence.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
	}

	// trim variation selectors surrounding sequence
	function trimVS(sequence) {
		const split = sequence.split(/^[\uFE00-\uFE0F]+|[\uFE00-\uFE0F]+$/);
		return split.filter(sequence => sequence.length>0)[0];
	}

	function isUndefined(variable) {
		return (typeof variable === 'undefined');
	}

	function set(setting, value) {
		settings[setting] = value;
	}
}());
<|endoftext|>