<commit_before>/**
* Copyright 2012-2016, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

var d3 = require('d3');
var isNumeric = require('fast-isnumeric');

var Lib = require('../../lib');
var svgTextUtils = require('../../lib/svg_text_utils');

var Color = require('../../components/color');
var Drawing = require('../../components/drawing');
var ErrorBars = require('../../components/errorbars');

var arraysToCalcdata = require('./arrays_to_calcdata');

module.exports = function plot(gd, plotinfo, cdbar) {
    var xa = plotinfo.xaxis,
        ya = plotinfo.yaxis,
        fullLayout = gd._fullLayout;

    var bartraces = plotinfo.plot.select('.barlayer')
        .selectAll('g.trace.bars')
            .data(cdbar)
      .enter().append('g')
        .attr('class', 'trace bars');

    bartraces.append('g')
        .attr('class', 'points')
        .each(function(d) {
            var t = d[0].t,
                trace = d[0].trace,
                poffset = t.poffset,
                poffsetIsArray = Array.isArray(poffset),
                barwidth = t.barwidth,
                barwidthIsArray = Array.isArray(barwidth);

            arraysToCalcdata(d);

            d3.select(this).selectAll('g.point')
                .data(Lib.identity)
                .enter().append('g').classed('point', true)
                .each(function(di, i) {
                    // now display the bar
                    // clipped xf/yf (2nd arg true): non-positive
                    // log values go off-screen by plotwidth
                    // so you see them continue if you drag the plot
                    var p0 = di.p + ((poffsetIsArray) ? poffset[i] : poffset),
                        p1 = p0 + ((barwidthIsArray) ? barwidth[i] : barwidth),
                        s0 = di.b,
                        s1 = s0 + di.s;

                    var x0, x1, y0, y1;
                    if(trace.orientation === 'h') {
                        y0 = ya.c2p(p0, true);
                        y1 = ya.c2p(p1, true);
                        x0 = xa.c2p(s0, true);
                        x1 = xa.c2p(s1, true);
                    }
                    else {
                        x0 = xa.c2p(p0, true);
                        x1 = xa.c2p(p1, true);
                        y0 = ya.c2p(s0, true);
                        y1 = ya.c2p(s1, true);
                    }

                    if(!isNumeric(x0) || !isNumeric(x1) ||
                            !isNumeric(y0) || !isNumeric(y1) ||
                            x0 === x1 || y0 === y1) {
                        d3.select(this).remove();
                        return;
                    }

                    var lw = (di.mlw + 1 || trace.marker.line.width + 1 ||
                            (di.trace ? di.trace.marker.line.width : 0) + 1) - 1,
                        offset = d3.round((lw / 2) % 1, 2);

                    function roundWithLine(v) {
                        // if there are explicit gaps, don't round,
                        // it can make the gaps look crappy
                        return (fullLayout.bargap === 0 && fullLayout.bargroupgap === 0) ?
                            d3.round(Math.round(v) - offset, 2) : v;
                    }

                    function expandToVisible(v, vc) {
                        // if it's not in danger of disappearing entirely,
                        // round more precisely
                        return Math.abs(v - vc) >= 2 ? roundWithLine(v) :
                        // but if it's very thin, expand it so it's
                        // necessarily visible, even if it might overlap
                        // its neighbor
                        (v > vc ? Math.ceil(v) : Math.floor(v));
                    }

                    if(!gd._context.staticPlot) {
                        // if bars are not fully opaque or they have a line
                        // around them, round to integer pixels, mainly for
                        // safari so we prevent overlaps from its expansive
                        // pixelation. if the bars ARE fully opaque and have
                        // no line, expand to a full pixel to make sure we
                        // can see them
                        var op = Color.opacity(di.mc || trace.marker.color),
                            fixpx = (op < 1 || lw > 0.01) ?
                                roundWithLine : expandToVisible;
                        x0 = fixpx(x0, x1);
                        x1 = fixpx(x1, x0);
                        y0 = fixpx(y0, y1);
                        y1 = fixpx(y1, y0);
                    }

                    // append bar path and text
                    var bar = d3.select(this);

                    bar.append('path').attr('d',
                        'M' + x0 + ',' + y0 + 'V' + y1 + 'H' + x1 + 'V' + y0 + 'Z');

                    appendBarText(gd, bar, d, i, x0, x1, y0, y1);
                });
        });

    // error bars are on the top
    bartraces.call(ErrorBars.plot, plotinfo);

};

function appendBarText(gd, bar, calcTrace, i, x0, x1, y0, y1) {
    var trace = calcTrace[0].trace;

    // get bar text
    var traceText = trace.text;
    if(!traceText) return;

    var text;
    if(Array.isArray(traceText)) {
        if(i >= traceText.length) return;
        text = traceText[i];
    }
    else {
        text = traceText;
    }

    // get text position
    var traceTextPosition = trace.textposition,
        textPosition;
    if(Array.isArray(traceTextPosition)) {
        if(i >= traceTextPosition.length) return;
        textPosition = traceTextPosition[i];
    }
    else {
        textPosition = traceTextPosition;
    }

    if(textPosition === 'none') return;

    var barWidth = Math.abs(x1 - x0),
        barHeight = Math.abs(y1 - y0),
        barIsTooSmall = (barWidth < 8 || barHeight < 8),

        barmode = gd._fullLayout.barmode,
        inStackMode = (barmode === 'stack'),
        inRelativeMode = (barmode === 'relative'),
        inStackOrRelativeMode = inStackMode || inRelativeMode,

        calcBar = calcTrace[i],
        isOutmostBar = !inStackOrRelativeMode || calcBar._outmost;

    if(textPosition === 'auto') {
        textPosition = (barIsTooSmall && isOutmostBar) ? 'outside' : 'inside';
    }

    if(textPosition === 'outside') {
        if(!isOutmostBar) textPosition = 'inside';
    }

    if(textPosition === 'inside') {
        if(barIsTooSmall) return;
    }

    // get text font
    var textFont;

    if(textPosition === 'outside') {
        var traceOutsideTextFont = trace.outsidetextfont;
        if(Array.isArray(traceOutsideTextFont)) {
            if(i >= traceOutsideTextFont.length) return;
            textFont = traceOutsideTextFont[i];
        }
        else {
            textFont = traceOutsideTextFont;
        }
    }
    else {
        var traceInsideTextFont = trace.insidetextfont;
        if(Array.isArray(traceInsideTextFont)) {
            if(i >= traceInsideTextFont.length) return;
            textFont = traceInsideTextFont[i];
        }
        else {
            textFont = traceInsideTextFont;
        }
    }

    if(!textFont) {
        var traceTextFont = trace.textfont;
        if(Array.isArray(traceTextFont)) {
            if(i >= traceTextFont.length) return;
            textFont = traceTextFont[i];
        }
        else {
            textFont = traceTextFont;
        }
    }

    if(!textFont) {
        textFont = gd._fullLayout.font;
    }

    // append bar text
    var textSelection = bar.append('text')
        // prohibit tex interpretation until we can handle
        // tex and regular text together
        .attr('data-notex', 1)
        .text(text)
        .attr({
            'class': 'bartext',
            transform: '',
            'data-bb': '',
            'text-anchor': 'middle',
            x: 0,
            y: 0
        })
        .call(Drawing.font, textFont);

    textSelection.call(svgTextUtils.convertToTspans);
    textSelection.selectAll('tspan.line').attr({x: 0, y: 0});

    // position bar text
    var textBB = Drawing.bBox(textSelection.node()),
        textWidth = textBB.width,
        textHeight = textBB.height;
    if(!textWidth || !textHeight) {
        textSelection.remove();
        return;
    }

    // compute translate transform
    var transform;
    if(textPosition === 'outside') {
        transform = getTransformToMoveOutsideBar(x0, x1, y0, y1, textBB,
            trace.orientation);
    }
    else {
        transform = getTransformToMoveInsideBar(x0, x1, y0, y1, textBB);
    }

    textSelection.attr('transform', transform);
}

function getTransformToMoveInsideBar(x0, x1, y0, y1, textBB) {
    // compute text and target positions
    var barWidth = Math.abs(x1 - x0),
        barHeight = Math.abs(y1 - y0),
        textWidth = textBB.width,
        textHeight = textBB.height,
        barX = (x0 + x1) / 2,
        barY = (y0 + y1) / 2,
        textX = (textBB.left + textBB.right) / 2,
        textY = (textBB.top + textBB.bottom) / 2;

    // apply target padding
    var targetWidth = 0.95 * barWidth,
        targetHeight = 0.95 * barHeight;

    return getTransform(
        textX, textY, textWidth, textHeight,
        barX, barY, targetWidth, targetHeight);
}

function getTransformToMoveOutsideBar(x0, x1, y0, y1, textBB, orientation) {

    // compute text and target positions
    var textWidth = textBB.width,
        textHeight = textBB.height,
        textX = (textBB.left + textBB.right) / 2,
        textY = (textBB.top + textBB.bottom) / 2;

    var targetWidth, targetHeight,
        targetX, targetY;
    if(orientation === 'h') {
        if(x1 < x0) {
            // bar end is on the left hand side
            targetWidth = 2 + textWidth; // padding included
            targetHeight = Math.abs(y1 - y0);
            targetX = x1 - targetWidth / 2;
            targetY = (y0 + y1) / 2;
        }
        else {
            targetWidth = 2 + textWidth; // padding included
            targetHeight = Math.abs(y1 - y0);
            targetX = x1 + targetWidth / 2;
            targetY = (y0 + y1) / 2;
        }
    }
    else {
        if(y1 > y0) {
            // bar end is on the bottom
            targetWidth = Math.abs(x1 - x0);
            targetHeight = 2 + textHeight; // padding included
            targetX = (x0 + x1) / 2;
            targetY = y1 + targetHeight / 2;
        }
        else {
            targetWidth = Math.abs(x1 - x0);
            targetHeight = 2 + textHeight; // padding included
            targetX = (x0 + x1) / 2;
            targetY = y1 - targetHeight / 2;
        }
    }

    return getTransform(
        textX, textY, textWidth, textHeight,
        targetX, targetY, targetWidth, targetHeight);
}

/**
 * Compute SVG transform to move a text box into a target box
 *
 * @param {number} textX         X pixel coord of the text box center
 * @param {number} textY         Y pixel coord of the text box center
 * @param {number} textWidth     text box width
 * @param {number} textHeight    text box height
 * @param {number} targetX       X pixel coord of the target box center
 * @param {number} targetY       Y pixel coord of the target box center
 * @param {number} targetWidth   target box width
 * @param {number} targetHeight  target box height
 *
 * @returns {string} SVG transform
 */
function getTransform(
    textX, textY, textWidth, textHeight,
    targetX, targetY, targetWidth, targetHeight) {

    // compute translate transform
    var translateX = targetX - textX,
        translateY = targetY - textY,
        translate = 'translate(' + translateX + ' ' + translateY + ')';

    // if bar text doesn't fit, compute rotate and scale transforms
    var doesntFit = (textWidth > targetWidth || textHeight > targetHeight),
        rotate, scale, scaleX, scaleY;

    if(doesntFit) {
        var textIsHorizontal = (textWidth > textHeight),
            targetIsHorizontal = (targetWidth > targetHeight);
        if(textIsHorizontal !== targetIsHorizontal) {
            rotate = 'rotate(-90 ' + textX + ' ' + textY + ')';
            scaleX = targetWidth / textHeight;
            scaleY = targetHeight / textWidth;
        }
        else {
            scaleX = targetWidth / textWidth;
            scaleY = targetHeight / textHeight;
        }

        if(scaleX > 1) scaleX = 1;
        if(scaleY > 1) scaleY = 1;

        if(scaleX !== 1 || scaleY !== 1) {
            scale = 'scale(' + scaleX + ' ' + scaleY + ')';
        }
    }

    // compute transform
    var transform = translate;
    if(scale) transform += ' ' + scale;
    if(rotate) transform += ' ' + rotate;

    return transform;
}
<commit_msg>bar: fix indentation of .enter()<commit_after>/**
* Copyright 2012-2016, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

var d3 = require('d3');
var isNumeric = require('fast-isnumeric');

var Lib = require('../../lib');
var svgTextUtils = require('../../lib/svg_text_utils');

var Color = require('../../components/color');
var Drawing = require('../../components/drawing');
var ErrorBars = require('../../components/errorbars');

var arraysToCalcdata = require('./arrays_to_calcdata');

module.exports = function plot(gd, plotinfo, cdbar) {
    var xa = plotinfo.xaxis,
        ya = plotinfo.yaxis,
        fullLayout = gd._fullLayout;

    var bartraces = plotinfo.plot.select('.barlayer')
        .selectAll('g.trace.bars')
            .data(cdbar)
      .enter().append('g')
        .attr('class', 'trace bars');

    bartraces.append('g')
        .attr('class', 'points')
        .each(function(d) {
            var t = d[0].t,
                trace = d[0].trace,
                poffset = t.poffset,
                poffsetIsArray = Array.isArray(poffset),
                barwidth = t.barwidth,
                barwidthIsArray = Array.isArray(barwidth);

            arraysToCalcdata(d);

            d3.select(this).selectAll('g.point')
                .data(Lib.identity)
              .enter().append('g').classed('point', true)
                .each(function(di, i) {
                    // now display the bar
                    // clipped xf/yf (2nd arg true): non-positive
                    // log values go off-screen by plotwidth
                    // so you see them continue if you drag the plot
                    var p0 = di.p + ((poffsetIsArray) ? poffset[i] : poffset),
                        p1 = p0 + ((barwidthIsArray) ? barwidth[i] : barwidth),
                        s0 = di.b,
                        s1 = s0 + di.s;

                    var x0, x1, y0, y1;
                    if(trace.orientation === 'h') {
                        y0 = ya.c2p(p0, true);
                        y1 = ya.c2p(p1, true);
                        x0 = xa.c2p(s0, true);
                        x1 = xa.c2p(s1, true);
                    }
                    else {
                        x0 = xa.c2p(p0, true);
                        x1 = xa.c2p(p1, true);
                        y0 = ya.c2p(s0, true);
                        y1 = ya.c2p(s1, true);
                    }

                    if(!isNumeric(x0) || !isNumeric(x1) ||
                            !isNumeric(y0) || !isNumeric(y1) ||
                            x0 === x1 || y0 === y1) {
                        d3.select(this).remove();
                        return;
                    }

                    var lw = (di.mlw + 1 || trace.marker.line.width + 1 ||
                            (di.trace ? di.trace.marker.line.width : 0) + 1) - 1,
                        offset = d3.round((lw / 2) % 1, 2);

                    function roundWithLine(v) {
                        // if there are explicit gaps, don't round,
                        // it can make the gaps look crappy
                        return (fullLayout.bargap === 0 && fullLayout.bargroupgap === 0) ?
                            d3.round(Math.round(v) - offset, 2) : v;
                    }

                    function expandToVisible(v, vc) {
                        // if it's not in danger of disappearing entirely,
                        // round more precisely
                        return Math.abs(v - vc) >= 2 ? roundWithLine(v) :
                        // but if it's very thin, expand it so it's
                        // necessarily visible, even if it might overlap
                        // its neighbor
                        (v > vc ? Math.ceil(v) : Math.floor(v));
                    }

                    if(!gd._context.staticPlot) {
                        // if bars are not fully opaque or they have a line
                        // around them, round to integer pixels, mainly for
                        // safari so we prevent overlaps from its expansive
                        // pixelation. if the bars ARE fully opaque and have
                        // no line, expand to a full pixel to make sure we
                        // can see them
                        var op = Color.opacity(di.mc || trace.marker.color),
                            fixpx = (op < 1 || lw > 0.01) ?
                                roundWithLine : expandToVisible;
                        x0 = fixpx(x0, x1);
                        x1 = fixpx(x1, x0);
                        y0 = fixpx(y0, y1);
                        y1 = fixpx(y1, y0);
                    }

                    // append bar path and text
                    var bar = d3.select(this);

                    bar.append('path').attr('d',
                        'M' + x0 + ',' + y0 + 'V' + y1 + 'H' + x1 + 'V' + y0 + 'Z');

                    appendBarText(gd, bar, d, i, x0, x1, y0, y1);
                });
        });

    // error bars are on the top
    bartraces.call(ErrorBars.plot, plotinfo);

};

function appendBarText(gd, bar, calcTrace, i, x0, x1, y0, y1) {
    var trace = calcTrace[0].trace;

    // get bar text
    var traceText = trace.text;
    if(!traceText) return;

    var text;
    if(Array.isArray(traceText)) {
        if(i >= traceText.length) return;
        text = traceText[i];
    }
    else {
        text = traceText;
    }

    // get text position
    var traceTextPosition = trace.textposition,
        textPosition;
    if(Array.isArray(traceTextPosition)) {
        if(i >= traceTextPosition.length) return;
        textPosition = traceTextPosition[i];
    }
    else {
        textPosition = traceTextPosition;
    }

    if(textPosition === 'none') return;

    var barWidth = Math.abs(x1 - x0),
        barHeight = Math.abs(y1 - y0),
        barIsTooSmall = (barWidth < 8 || barHeight < 8),

        barmode = gd._fullLayout.barmode,
        inStackMode = (barmode === 'stack'),
        inRelativeMode = (barmode === 'relative'),
        inStackOrRelativeMode = inStackMode || inRelativeMode,

        calcBar = calcTrace[i],
        isOutmostBar = !inStackOrRelativeMode || calcBar._outmost;

    if(textPosition === 'auto') {
        textPosition = (barIsTooSmall && isOutmostBar) ? 'outside' : 'inside';
    }

    if(textPosition === 'outside') {
        if(!isOutmostBar) textPosition = 'inside';
    }

    if(textPosition === 'inside') {
        if(barIsTooSmall) return;
    }

    // get text font
    var textFont;

    if(textPosition === 'outside') {
        var traceOutsideTextFont = trace.outsidetextfont;
        if(Array.isArray(traceOutsideTextFont)) {
            if(i >= traceOutsideTextFont.length) return;
            textFont = traceOutsideTextFont[i];
        }
        else {
            textFont = traceOutsideTextFont;
        }
    }
    else {
        var traceInsideTextFont = trace.insidetextfont;
        if(Array.isArray(traceInsideTextFont)) {
            if(i >= traceInsideTextFont.length) return;
            textFont = traceInsideTextFont[i];
        }
        else {
            textFont = traceInsideTextFont;
        }
    }

    if(!textFont) {
        var traceTextFont = trace.textfont;
        if(Array.isArray(traceTextFont)) {
            if(i >= traceTextFont.length) return;
            textFont = traceTextFont[i];
        }
        else {
            textFont = traceTextFont;
        }
    }

    if(!textFont) {
        textFont = gd._fullLayout.font;
    }

    // append bar text
    var textSelection = bar.append('text')
        // prohibit tex interpretation until we can handle
        // tex and regular text together
        .attr('data-notex', 1)
        .text(text)
        .attr({
            'class': 'bartext',
            transform: '',
            'data-bb': '',
            'text-anchor': 'middle',
            x: 0,
            y: 0
        })
        .call(Drawing.font, textFont);

    textSelection.call(svgTextUtils.convertToTspans);
    textSelection.selectAll('tspan.line').attr({x: 0, y: 0});

    // position bar text
    var textBB = Drawing.bBox(textSelection.node()),
        textWidth = textBB.width,
        textHeight = textBB.height;
    if(!textWidth || !textHeight) {
        textSelection.remove();
        return;
    }

    // compute translate transform
    var transform;
    if(textPosition === 'outside') {
        transform = getTransformToMoveOutsideBar(x0, x1, y0, y1, textBB,
            trace.orientation);
    }
    else {
        transform = getTransformToMoveInsideBar(x0, x1, y0, y1, textBB);
    }

    textSelection.attr('transform', transform);
}

function getTransformToMoveInsideBar(x0, x1, y0, y1, textBB) {
    // compute text and target positions
    var barWidth = Math.abs(x1 - x0),
        barHeight = Math.abs(y1 - y0),
        textWidth = textBB.width,
        textHeight = textBB.height,
        barX = (x0 + x1) / 2,
        barY = (y0 + y1) / 2,
        textX = (textBB.left + textBB.right) / 2,
        textY = (textBB.top + textBB.bottom) / 2;

    // apply target padding
    var targetWidth = 0.95 * barWidth,
        targetHeight = 0.95 * barHeight;

    return getTransform(
        textX, textY, textWidth, textHeight,
        barX, barY, targetWidth, targetHeight);
}

function getTransformToMoveOutsideBar(x0, x1, y0, y1, textBB, orientation) {

    // compute text and target positions
    var textWidth = textBB.width,
        textHeight = textBB.height,
        textX = (textBB.left + textBB.right) / 2,
        textY = (textBB.top + textBB.bottom) / 2;

    var targetWidth, targetHeight,
        targetX, targetY;
    if(orientation === 'h') {
        if(x1 < x0) {
            // bar end is on the left hand side
            targetWidth = 2 + textWidth; // padding included
            targetHeight = Math.abs(y1 - y0);
            targetX = x1 - targetWidth / 2;
            targetY = (y0 + y1) / 2;
        }
        else {
            targetWidth = 2 + textWidth; // padding included
            targetHeight = Math.abs(y1 - y0);
            targetX = x1 + targetWidth / 2;
            targetY = (y0 + y1) / 2;
        }
    }
    else {
        if(y1 > y0) {
            // bar end is on the bottom
            targetWidth = Math.abs(x1 - x0);
            targetHeight = 2 + textHeight; // padding included
            targetX = (x0 + x1) / 2;
            targetY = y1 + targetHeight / 2;
        }
        else {
            targetWidth = Math.abs(x1 - x0);
            targetHeight = 2 + textHeight; // padding included
            targetX = (x0 + x1) / 2;
            targetY = y1 - targetHeight / 2;
        }
    }

    return getTransform(
        textX, textY, textWidth, textHeight,
        targetX, targetY, targetWidth, targetHeight);
}

/**
 * Compute SVG transform to move a text box into a target box
 *
 * @param {number} textX         X pixel coord of the text box center
 * @param {number} textY         Y pixel coord of the text box center
 * @param {number} textWidth     text box width
 * @param {number} textHeight    text box height
 * @param {number} targetX       X pixel coord of the target box center
 * @param {number} targetY       Y pixel coord of the target box center
 * @param {number} targetWidth   target box width
 * @param {number} targetHeight  target box height
 *
 * @returns {string} SVG transform
 */
function getTransform(
    textX, textY, textWidth, textHeight,
    targetX, targetY, targetWidth, targetHeight) {

    // compute translate transform
    var translateX = targetX - textX,
        translateY = targetY - textY,
        translate = 'translate(' + translateX + ' ' + translateY + ')';

    // if bar text doesn't fit, compute rotate and scale transforms
    var doesntFit = (textWidth > targetWidth || textHeight > targetHeight),
        rotate, scale, scaleX, scaleY;

    if(doesntFit) {
        var textIsHorizontal = (textWidth > textHeight),
            targetIsHorizontal = (targetWidth > targetHeight);
        if(textIsHorizontal !== targetIsHorizontal) {
            rotate = 'rotate(-90 ' + textX + ' ' + textY + ')';
            scaleX = targetWidth / textHeight;
            scaleY = targetHeight / textWidth;
        }
        else {
            scaleX = targetWidth / textWidth;
            scaleY = targetHeight / textHeight;
        }

        if(scaleX > 1) scaleX = 1;
        if(scaleY > 1) scaleY = 1;

        if(scaleX !== 1 || scaleY !== 1) {
            scale = 'scale(' + scaleX + ' ' + scaleY + ')';
        }
    }

    // compute transform
    var transform = translate;
    if(scale) transform += ' ' + scale;
    if(rotate) transform += ' ' + rotate;

    return transform;
}
<|endoftext|>


<commit_before>const AsciiTable = require('ascii-table');
const assert = require('assert');
const num = require('num');

const { sequelize } = require('./sequelize');
const { Account, Disposal, Transaction } = require('./models');

async function getCurrencies() {
  return Account.aggregate('currency', 'distinct', { plain: false })
    .map(f => f.distinct);
}

module.exports.printSummary = async function printSummary(config) {
  console.log('\n\nSummary:');

  const [summary] = await sequelize.query(`
      select
        extract(year from "disposedAt") as year,
        currency,
        term,
        sum(gain) as gain
      from disposals
      group by 1, 2, 3
      order by 1, 2, 3
  `);

  const years = {};
  for (const row of summary) {
    years[row.year] = years[row.year] || {};
    years[row.year].total = years[row.year].total || {
      short: num(0),
      long: num(0),
      total: num(0),
    };

    // totals per currency
    years[row.year][row.currency] = years[row.year][row.currency] || {
      short: num(0),
      long: num(0),
      total: num(0),
    };
    years[row.year][row.currency][row.term] = num(row.gain);
    years[row.year][row.currency].total = years[row.year][row.currency].total.add(row.gain);

    // totals for all currencies
    years[row.year].total[row.term] = years[row.year].total[row.term].add(row.gain);
    years[row.year].total.total = years[row.year].total.total.add(row.gain);
  }

  const table = new AsciiTable();
  table.setHeading('year', 'currency', 'method', 'short', 'long', 'total');
  table.setAlignRight(3);
  table.setAlignRight(4);
  table.setAlignRight(5);

  for (const year of Object.keys(years)) {
    for (const currency of Object.keys(years[year])) {
      const row = years[year][currency];
      if (currency !== 'total' && !row.total.eq(0)) {
        table.addRow(
          year,
          currency,
          config.getDisposalMethod(year).method,
          row.short.toString(),
          row.long.toString(),
          row.total.toString()
        );
      }
    }
    table.addRow(
      year,
      'TOTAL',
      config.getDisposalMethod(year).method,
      years[year].total.short.toString(),
      years[year].total.long.toString(),
      years[year].total.total.toString()
    );
    table.addRow();
  }

  console.log(table.toString());
};

// find hold in specific category (short/long, loss/gain) with higest cost basis
function findSpecificLot(tx, holds, term, gainOrLoss) {
  const shortTermCutoffDate = new Date(tx.timestamp.toString());
  shortTermCutoffDate.setFullYear(shortTermCutoffDate.getFullYear() - 1);

  function isLongTerm(timestamp) {
    return timestamp < shortTermCutoffDate;
  }

  if (!['short', 'long'].includes(term)) {
    throw new Error(`invalid term ${term}`);
  }

  if (!['gain', 'loss'].includes(gainOrLoss)) {
    throw new Error(`invalid gainOrLoss ${gainOrLoss}`);
  }

  let highestUsdPrice;
  let bestHold = null;
  for (const hold of holds) {
    // skip if incorrect term
    if (term === 'short' && isLongTerm(hold.timestamp)) {
      continue;
    }
    if (term === 'long' && !isLongTerm(hold.timestamp)) {
      continue;
    }

    // skip if incorrect gainOrLoss
    const usdPriceGain = num(tx.usdPrice).sub(hold.usdPrice);
    if (gainOrLoss === 'gain' && usdPriceGain.lt(0)) {
      continue;
    }
    if (gainOrLoss === 'loss' && usdPriceGain.gte(0)) {
      continue;
    }

    // search for highest usdPrice (cost basis per unit)
    if (highestUsdPrice === undefined || highestUsdPrice.lt(hold.usdPrice)) {
      bestHold = hold;
      highestUsdPrice = num(hold.usdPrice);
    }
  }

  // if we exhaused all holds in this category this will just return null
  return bestHold;
}

// TaxMin: https://www.betterment.com/resources/lowering-your-tax-bill-by-improving-our-cost-basis-accounting-methods/
// Sell in following order:
// 1. Short-term losses
// 2. Long-term losses
// 3. Long-term gains
// 4. Short-term gains
function findHoldTaxMin(tx, holds) {
  let hold;

  hold = findSpecificLot(tx, holds, 'short', 'loss');
  if (hold) return hold;

  hold = findSpecificLot(tx, holds, 'long', 'loss');
  if (hold) return hold;

  hold = findSpecificLot(tx, holds, 'long', 'gain');
  if (hold) return hold;

  hold = findSpecificLot(tx, holds, 'short', 'gain');
  if (hold) return hold;

  return null;
}

// Estimate strategy: calculate the estimated tax due for each hold,
// and select the hold which causes the lowest tax liability
function findMinimizedTaxHold(amountRemaining, tx, holds, method) {
  // console.log('\nfindMinimizedTaxHold:',
  //   tx.timestamp,
  //   tx.amount,
  //   amountRemaining.toString(),
  //   tx.usdPrice,
  //   `${holds.length} holds`);

  const shortTermCutoffDate = new Date(tx.timestamp.toString());
  shortTermCutoffDate.setFullYear(shortTermCutoffDate.getFullYear() - 1);

  function isLongTerm(timestamp) {
    return timestamp < shortTermCutoffDate;
  }

  // search for lot to sell which causes lowest tax due
  let lowestTaxPerUnit;
  let lowestTaxHold;

  for (const hold of holds) {
    // calculate amount we could dispose of with this hold
    let disposalAmount;
    if (num(hold.amount).gt(amountRemaining)) {
      disposalAmount = amountRemaining;
    } else {
      disposalAmount = num(hold.amount);
    }

    // calculate potential gain for this hold
    const costBasis = disposalAmount.mul(hold.usdPrice);
    const salePrice = disposalAmount.mul(tx.usdPrice);
    const gain = salePrice.sub(costBasis);

    // calculate estimated tax for this hold
    let estimatedTaxDue;
    if (isLongTerm(hold.timestamp)) {
      estimatedTaxDue = gain.mul(method.longTermTaxRate);
    } else {
      estimatedTaxDue = gain.mul(method.shortTermTaxRate);
    }

    // normalize estimated tax per unit so that we don't bias selling small lots
    const estimatedTaxPerUnit = estimatedTaxDue.div(disposalAmount);

    // console.log(
    //   isLongTerm(hold.timestamp) ? 'long' : 'short',
    //   hold.timestamp,
    //   hold.usdPrice,
    //   disposalAmount.toString(),
    //   gain.toString(),
    //   estimatedTaxDue.toString());

    if (lowestTaxPerUnit === undefined || lowestTaxPerUnit.gt(estimatedTaxPerUnit)) {
      lowestTaxPerUnit = estimatedTaxPerUnit;
      lowestTaxHold = hold;
    }
  }

  // console.log('found:',
  //   isLongTerm(lowestTaxHold.timestamp) ? 'long' : 'short',
  //   lowestTaxPerUnit.toString(),
  //   lowestTaxHold.timestamp,
  //   tx.usdPrice,
  //   lowestTaxHold.usdPrice,
  //   lowestTaxHold.amount);

  return lowestTaxHold;
}

async function calculateGainsForCurrency(currency, config) {
  console.log(`\nCalculating gains (${currency})`);

  // load all transactions
  const transactions = await Transaction.findAll({
    where: {
      currency,
      type: { $ne: 'transfer' },
    },
    order: [['timestamp', 'asc']],
  });

  const holds = [];
  for (const tx of transactions) {
    // console.log(tx.timestamp, tx.type, tx.currency, tx.amount);

    // verify usdPrice
    if (!tx.usdPrice) {
      console.error(
        '\ntransaction missing usdPrice:',
        tx.timestamp,
        tx.type,
        tx.currency,
        tx.amount
      );
      return;
    }

    if (tx.amount > 0) {
      // buy or receive: add to holds
      holds.push(tx);
    } else {
      // sell or send: dispose of holds
      let amountRemaining = num(tx.amount).abs();
      while (amountRemaining.gt(0)) {
        process.stdout.write('+');

        if (holds.length === 0) {
          process.stdout.write('\n');
          console.log('transaction:', tx.get());
          console.log({ amountRemaining: amountRemaining.toString() });
          throw Error('tried to dispose with no available holds: probably missing transactions.');
        }

        // find hold according to disposal strategy for this year
        let hold;
        const method = config.getDisposalMethod(tx.timestamp.getFullYear());
        if (method.method === 'FIFO') {
          hold = holds[0];
        } else if (method.method === 'LIFO') {
          hold = holds[holds.length - 1];
        } else if (method.method === 'TaxMin') {
          hold = findHoldTaxMin(tx, holds);
        } else if (method.method === 'Estimate') {
          hold = findMinimizedTaxHold(amountRemaining, tx, holds, method);
        } else {
          throw new Error(`invalid disposalMethod for ${tx.timestamp.getFullYear()}: ${method.method}`);
        }
        assert.ok(hold);
        hold.amount = num(hold.amount);

        const disposal = Disposal.build({
          currency,
          buyTransactionId: hold.id,
          sellTransactionId: tx.id,
          acquiredAt: hold.timestamp,
          disposedAt: tx.timestamp,
        });

        if (hold.amount.gt(amountRemaining)) {
          // reduce hold by amountRemaining, then we are done
          hold.amount = hold.amount.sub(amountRemaining).toString();
          disposal.amount = amountRemaining;
        } else {
          // dispose of this entire hold and continue loop
          disposal.amount = hold.amount;

          // remove this hold from holds array
          const prevHoldsLength = holds.length;
          const holdIndex = holds.indexOf(hold);
          if (holdIndex >= 0) {
            holds.splice(holdIndex, 1);
          }
          assert.ok(prevHoldsLength === holds.length + 1);
        }

        // calculate gain
        amountRemaining = amountRemaining.sub(disposal.amount);
        disposal.costBasis = disposal.amount.mul(hold.usdPrice);
        disposal.salePrice = disposal.amount.mul(tx.usdPrice);
        disposal.gain = disposal.salePrice.sub(disposal.costBasis);

        // convert back to 2dp
        disposal.amount = disposal.amount.toString();
        disposal.costBasis = parseFloat(disposal.costBasis.toString()).toFixed(2);
        disposal.salePrice = parseFloat(disposal.salePrice.toString()).toFixed(2);
        disposal.gain = parseFloat(disposal.gain.toString()).toFixed(2);

        await disposal.save();
      }
    }
  }
}

module.exports.truncateGains = async function truncateGains() {
  console.log('\nTruncate gains');
  await Disposal.truncate();
};

module.exports.calculateGains = async function calculateGains(config) {
  await module.exports.truncateGains();

  const currencies = await getCurrencies();

  for (const currency of currencies) {
    await calculateGainsForCurrency(currency, config);
  }
};
<commit_msg>Add LossThenFIFO disposal method<commit_after>const AsciiTable = require('ascii-table');
const assert = require('assert');
const num = require('num');

const { sequelize } = require('./sequelize');
const { Account, Disposal, Transaction } = require('./models');

async function getCurrencies() {
  return Account.aggregate('currency', 'distinct', { plain: false })
    .map(f => f.distinct);
}

module.exports.printSummary = async function printSummary(config) {
  console.log('\n\nSummary:');

  const [summary] = await sequelize.query(`
      select
        extract(year from "disposedAt") as year,
        currency,
        term,
        sum(gain) as gain
      from disposals
      group by 1, 2, 3
      order by 1, 2, 3
  `);

  const years = {};
  for (const row of summary) {
    years[row.year] = years[row.year] || {};
    years[row.year].total = years[row.year].total || {
      short: num(0),
      long: num(0),
      total: num(0),
    };

    // totals per currency
    years[row.year][row.currency] = years[row.year][row.currency] || {
      short: num(0),
      long: num(0),
      total: num(0),
    };
    years[row.year][row.currency][row.term] = num(row.gain);
    years[row.year][row.currency].total = years[row.year][row.currency].total.add(row.gain);

    // totals for all currencies
    years[row.year].total[row.term] = years[row.year].total[row.term].add(row.gain);
    years[row.year].total.total = years[row.year].total.total.add(row.gain);
  }

  const table = new AsciiTable();
  table.setHeading('year', 'currency', 'method', 'short', 'long', 'total');
  table.setAlignRight(3);
  table.setAlignRight(4);
  table.setAlignRight(5);

  for (const year of Object.keys(years)) {
    for (const currency of Object.keys(years[year])) {
      const row = years[year][currency];
      if (currency !== 'total' && !row.total.eq(0)) {
        table.addRow(
          year,
          currency,
          config.getDisposalMethod(year).method,
          row.short.toString(),
          row.long.toString(),
          row.total.toString()
        );
      }
    }
    table.addRow(
      year,
      'TOTAL',
      config.getDisposalMethod(year).method,
      years[year].total.short.toString(),
      years[year].total.long.toString(),
      years[year].total.total.toString()
    );
    table.addRow();
  }

  console.log(table.toString());
};

// find hold in specific category (short/long, loss/gain) with higest cost basis
function findSpecificLot(tx, holds, term, gainOrLoss) {
  const shortTermCutoffDate = new Date(tx.timestamp.toString());
  shortTermCutoffDate.setFullYear(shortTermCutoffDate.getFullYear() - 1);

  function isLongTerm(timestamp) {
    return timestamp < shortTermCutoffDate;
  }

  if (!['short', 'long'].includes(term)) {
    throw new Error(`invalid term ${term}`);
  }

  if (!['gain', 'loss'].includes(gainOrLoss)) {
    throw new Error(`invalid gainOrLoss ${gainOrLoss}`);
  }

  let highestUsdPrice;
  let bestHold = null;
  for (const hold of holds) {
    // skip if incorrect term
    if (term === 'short' && isLongTerm(hold.timestamp)) {
      continue;
    }
    if (term === 'long' && !isLongTerm(hold.timestamp)) {
      continue;
    }

    // skip if incorrect gainOrLoss
    const usdPriceGain = num(tx.usdPrice).sub(hold.usdPrice);
    if (gainOrLoss === 'gain' && usdPriceGain.lt(0)) {
      continue;
    }
    if (gainOrLoss === 'loss' && usdPriceGain.gte(0)) {
      continue;
    }

    // search for highest usdPrice (cost basis per unit)
    if (highestUsdPrice === undefined || highestUsdPrice.lt(hold.usdPrice)) {
      bestHold = hold;
      highestUsdPrice = num(hold.usdPrice);
    }
  }

  // if we exhaused all holds in this category this will just return null
  return bestHold;
}

// TaxMin: https://www.betterment.com/resources/lowering-your-tax-bill-by-improving-our-cost-basis-accounting-methods/
// Sell in following order:
// 1. Short-term losses
// 2. Long-term losses
// 3. Long-term gains
// 4. Short-term gains
function findHoldTaxMin(tx, holds) {
  let hold;

  hold = findSpecificLot(tx, holds, 'short', 'loss');
  if (hold) return hold;

  hold = findSpecificLot(tx, holds, 'long', 'loss');
  if (hold) return hold;

  hold = findSpecificLot(tx, holds, 'long', 'gain');
  if (hold) return hold;

  hold = findSpecificLot(tx, holds, 'short', 'gain');
  if (hold) return hold;

  return null;
}

// LossThenFIFO
// Sell in following order:
// 1. Short-term losses
// 2. Long-term losses
// 3. Gains (FIFO)
// Difference from 'TaxMin' is that we don't bother selling gains HIFO
function findHoldLossThenFIFO(tx, holds) {
  let hold;

  hold = findSpecificLot(tx, holds, 'short', 'loss');
  if (hold) return hold;

  hold = findSpecificLot(tx, holds, 'long', 'loss');
  if (hold) return hold;

  // if no losses, default to FIFO
  return holds[0];
}

// Estimate strategy: calculate the estimated tax due for each hold,
// and select the hold which causes the lowest tax liability
function findMinimizedTaxHold(amountRemaining, tx, holds, method) {
  // console.log('\nfindMinimizedTaxHold:',
  //   tx.timestamp,
  //   tx.amount,
  //   amountRemaining.toString(),
  //   tx.usdPrice,
  //   `${holds.length} holds`);

  const shortTermCutoffDate = new Date(tx.timestamp.toString());
  shortTermCutoffDate.setFullYear(shortTermCutoffDate.getFullYear() - 1);

  function isLongTerm(timestamp) {
    return timestamp < shortTermCutoffDate;
  }

  // search for lot to sell which causes lowest tax due
  let lowestTaxPerUnit;
  let lowestTaxHold;

  for (const hold of holds) {
    // calculate amount we could dispose of with this hold
    let disposalAmount;
    if (num(hold.amount).gt(amountRemaining)) {
      disposalAmount = amountRemaining;
    } else {
      disposalAmount = num(hold.amount);
    }

    // calculate potential gain for this hold
    const costBasis = disposalAmount.mul(hold.usdPrice);
    const salePrice = disposalAmount.mul(tx.usdPrice);
    const gain = salePrice.sub(costBasis);

    // calculate estimated tax for this hold
    let estimatedTaxDue;
    if (isLongTerm(hold.timestamp)) {
      estimatedTaxDue = gain.mul(method.longTermTaxRate);
    } else {
      estimatedTaxDue = gain.mul(method.shortTermTaxRate);
    }

    // normalize estimated tax per unit so that we don't bias selling small lots
    const estimatedTaxPerUnit = estimatedTaxDue.div(disposalAmount);

    // console.log(
    //   isLongTerm(hold.timestamp) ? 'long' : 'short',
    //   hold.timestamp,
    //   hold.usdPrice,
    //   disposalAmount.toString(),
    //   gain.toString(),
    //   estimatedTaxDue.toString());

    if (lowestTaxPerUnit === undefined || lowestTaxPerUnit.gt(estimatedTaxPerUnit)) {
      lowestTaxPerUnit = estimatedTaxPerUnit;
      lowestTaxHold = hold;
    }
  }

  // console.log('found:',
  //   isLongTerm(lowestTaxHold.timestamp) ? 'long' : 'short',
  //   lowestTaxPerUnit.toString(),
  //   lowestTaxHold.timestamp,
  //   tx.usdPrice,
  //   lowestTaxHold.usdPrice,
  //   lowestTaxHold.amount);

  return lowestTaxHold;
}

async function calculateGainsForCurrency(currency, config) {
  console.log(`\nCalculating gains (${currency})`);

  // load all transactions
  const transactions = await Transaction.findAll({
    where: {
      currency,
      type: { $ne: 'transfer' },
    },
    order: [['timestamp', 'asc']],
  });

  const holds = [];
  for (const tx of transactions) {
    // console.log(tx.timestamp, tx.type, tx.currency, tx.amount);

    // verify usdPrice
    if (!tx.usdPrice) {
      console.error(
        '\ntransaction missing usdPrice:',
        tx.timestamp,
        tx.type,
        tx.currency,
        tx.amount
      );
      return;
    }

    if (tx.transferTransactionId) {
      console.error(
        '\nnon-transfer transaction should not have transferTransactionId:',
        tx.id,
        tx.type,
        tx.transferTransactionId,
        tx.timestamp,
        tx.currency,
        tx.amount
      );
      return;
    }

    if (tx.amount > 0) {
      // buy or receive: add to holds
      holds.push(tx);
    } else {
      // sell or send: dispose of holds
      let amountRemaining = num(tx.amount).abs();
      while (amountRemaining.gt(0)) {
        process.stdout.write('+');

        if (holds.length === 0) {
          process.stdout.write('\n');
          console.log('transaction:', tx.get());
          console.log({ amountRemaining: amountRemaining.toString() });
          throw Error('tried to dispose with no available holds: probably missing transactions.');
        }

        // find hold according to disposal strategy for this year
        let hold;
        const method = config.getDisposalMethod(tx.timestamp.getFullYear());
        if (method.method === 'FIFO') {
          hold = holds[0];
        } else if (method.method === 'LIFO') {
          hold = holds[holds.length - 1];
        } else if (method.method === 'TaxMin') {
          hold = findHoldTaxMin(tx, holds);
        } else if (method.method === 'LossThenFIFO') {
          hold = findHoldLossThenFIFO(tx, holds);
        } else if (method.method === 'Estimate') {
          hold = findMinimizedTaxHold(amountRemaining, tx, holds, method);
        } else {
          throw new Error(`invalid disposalMethod for ${tx.timestamp.getFullYear()}: ${method.method}`);
        }
        assert.ok(hold);
        hold.amount = num(hold.amount);

        const disposal = Disposal.build({
          currency,
          buyTransactionId: hold.id,
          sellTransactionId: tx.id,
          acquiredAt: hold.timestamp,
          disposedAt: tx.timestamp,
        });

        if (hold.amount.gt(amountRemaining)) {
          // reduce hold by amountRemaining, then we are done
          hold.amount = hold.amount.sub(amountRemaining).toString();
          disposal.amount = amountRemaining;
        } else {
          // dispose of this entire hold and continue loop
          disposal.amount = hold.amount;

          // remove this hold from holds array
          const prevHoldsLength = holds.length;
          const holdIndex = holds.indexOf(hold);
          if (holdIndex >= 0) {
            holds.splice(holdIndex, 1);
          }
          assert.ok(prevHoldsLength === holds.length + 1);
        }

        // calculate gain
        amountRemaining = amountRemaining.sub(disposal.amount);
        assert.ok(amountRemaining.gte(0));
        disposal.costBasis = disposal.amount.mul(hold.usdPrice);
        disposal.salePrice = disposal.amount.mul(tx.usdPrice);
        disposal.gain = disposal.salePrice.sub(disposal.costBasis);

        // convert back to 2dp
        disposal.amount = disposal.amount.toString();
        disposal.costBasis = parseFloat(disposal.costBasis.toString()).toFixed(2);
        disposal.salePrice = parseFloat(disposal.salePrice.toString()).toFixed(2);
        disposal.gain = parseFloat(disposal.gain.toString()).toFixed(2);

        await disposal.save();
      }
    }
  }
}

module.exports.truncateGains = async function truncateGains() {
  console.log('\nTruncate gains');
  await Disposal.truncate();
};

module.exports.calculateGains = async function calculateGains(config) {
  await module.exports.truncateGains();

  const currencies = await getCurrencies();

  for (const currency of currencies) {
    await calculateGainsForCurrency(currency, config);
  }
};
<|endoftext|>


<commit_before>var DISCORD = (function(){
  var getTopMessageViewElement = function(){
    let view = DOM.queryReactClass("messages");
    return view && view.children.length && view.children[0];
  };

  var observerTimer = 0, waitingForCleanup = 0;

  return {
    /*
     * Sets up a callback hook to trigger whenever the list of messages is updated. The callback is given a boolean value that is true if there are more messages to load.
     */
    setupMessageUpdateCallback: function(callback){
      var onTimerFinished = function(){
        let topEle = getTopMessageViewElement();

        if (!topEle){
          restartTimer(500);
        }
        else if (!topEle.getAttribute("class").includes("loadingMore-")){
          let messages = DOM.queryReactClass("messages").children.length;

          if (messages < 100){
            waitingForCleanup = 0;
          }

          if (waitingForCleanup > 0){
            --waitingForCleanup;
            restartTimer(750);
          }
          else{
            if (messages > 300){
              waitingForCleanup = 6;

              DOM.setTimer(() => {
                let view = DOM.queryReactClass("messages");
                view.scrollTop = view.scrollHeight/2;
              }, 1);
            }

            callback(topEle.getAttribute("class").includes("hasMore-"));
            restartTimer(200);
          }
        }
        else{
          restartTimer(25);
        }
      };

      var restartTimer = function(delay){
        observerTimer = DOM.setTimer(onTimerFinished, delay);
      };

      onTimerFinished();
      window.DHT_ON_UNLOAD.push(() => window.clearInterval(observerTimer));
    },

    /*
     * Returns internal React state object of an element.
     */
    getReactProps: function(ele){
      var key = Object.keys(ele || {}).find(key => key.startsWith("__reactInternalInstance"));
      return key ? ele[key].memoizedProps : null;
    },

    /*
     * Returns an object containing the selected server name, selected channel name and ID, and the object type.
     * For types DM and GROUP, the server and channel names are identical.
     * For SERVER type, the channel has to be in view, otherwise Discord unloads it.
     */
    getSelectedChannel: function(){
      try{
        var obj;
        var channelListEle = document.querySelector("[class|='privateChannels']");

        if (channelListEle){
          var channel = DOM.queryReactClass("selected", channelListEle);

          if (!channel){
            return null;
          }

          var linkSplit = channel.querySelector("a[href*='/@me/']").href.split("/");
          var link = linkSplit[linkSplit.length-1];

          if (!(/^\d+$/.test(link))){
            return null;
          }

          var name = Array.prototype.find.call(channel.querySelector("[class|='name']").childNodes, node => node.nodeType === Node.TEXT_NODE).nodeValue;

          obj = {
            "server": name,
            "channel": name,
            "id": link,
            "type": !!DOM.queryReactClass("status", channel) ? "DM" : "GROUP"
          };
        }
        else{
          channelListEle = document.querySelector("[class|='channels']");

          var channel = channelListEle.querySelector("[class|='wrapperSelectedText']").parentElement;
          var props = DISCORD.getReactProps(channel);

          if (!props){
            return null;
          }

          var channelObj = props.children.props.channel;

          if (!channelObj){
            return null;
          }

          obj = {
            "server": channelListEle.querySelector("header > span").innerHTML,
            "channel": channelObj.name,
            "id": channelObj.id,
            "type": "SERVER"
          };
        }

        return obj.channel.length === 0 ? null : obj;
      }catch(e){
        return null;
      }
    },

    /*
     * Returns an array containing currently loaded messages.
     */
    getMessages: function(){
      var props = DISCORD.getReactProps(DOM.queryReactClass("messages"));
      var array = props && props.children.find(ele => ele && ele.length);
      var messages = [];

      if (array){
        for(let obj of array){
          if (obj.props.messages){
            Array.prototype.push.apply(messages, obj.props.messages);
          }
        }
      }

      return messages;
    },

    /*
     * Returns true if the message view is visible.
     */
    isInMessageView: () => !!DOM.queryReactClass("messages"),

    /*
     * Returns true if there are more messages available or if they're still loading.
     */
    hasMoreMessages: function(){
      let classes = getTopMessageViewElement().getAttribute("class");
      return classes.includes("hasMore-") || classes.includes("loadingMore-");
    },

    /*
     * Forces the message view to load older messages by scrolling all the way up.
     */
    loadOlderMessages: function(){
      let view = DOM.queryReactClass("messages");
      view.scrollTop = view.scrollHeight/2;
      view.scrollTop = 0;
    },

    /*
     * Selects the next text channel and returns true, otherwise returns false if there are no more channels.
     */
    selectNextTextChannel: function(){
      var dms = document.querySelector("[class|='privateChannels']");

      if (dms){
        var nextChannel = DOM.queryReactClass("selected", dms).nextElementSibling;
        var nextLink = nextChannel && nextChannel.querySelector("a[href*='/@me/']");

        if (!nextChannel || !nextLink || !nextChannel.getAttribute("class").includes("channel-")){
          return false;
        }
        else{
          nextLink.click();
          return true;
        }
      }
      else{
        var allChannels = document.querySelector("[class|='channels']").querySelectorAll("[class|='containerDefault']");
        var nextChannel = null;

        for(var index = 0; index < allChannels.length-1; index++){
          if (allChannels[index].children[0].className.includes("wrapperSelectedText")){
            var next = allChannels[index+1];

            if (next.childElementCount > 0 && /wrapper([a-zA-Z]+?)Text/.test(next.children[0].className)){
              nextChannel = allChannels[index+1];
              break;
            }
          }
        }

        if (nextChannel === null){
          return false;
        }
        else{
          nextChannel.children[0].click();
          return true;
        }
      }
    }
  };
})();
<commit_msg>Fix server groups breaking the 'Swich to Next Channel' option<commit_after>var DISCORD = (function(){
  var getTopMessageViewElement = function(){
    let view = DOM.queryReactClass("messages");
    return view && view.children.length && view.children[0];
  };

  var observerTimer = 0, waitingForCleanup = 0;

  return {
    /*
     * Sets up a callback hook to trigger whenever the list of messages is updated. The callback is given a boolean value that is true if there are more messages to load.
     */
    setupMessageUpdateCallback: function(callback){
      var onTimerFinished = function(){
        let topEle = getTopMessageViewElement();

        if (!topEle){
          restartTimer(500);
        }
        else if (!topEle.getAttribute("class").includes("loadingMore-")){
          let messages = DOM.queryReactClass("messages").children.length;

          if (messages < 100){
            waitingForCleanup = 0;
          }

          if (waitingForCleanup > 0){
            --waitingForCleanup;
            restartTimer(750);
          }
          else{
            if (messages > 300){
              waitingForCleanup = 6;

              DOM.setTimer(() => {
                let view = DOM.queryReactClass("messages");
                view.scrollTop = view.scrollHeight/2;
              }, 1);
            }

            callback(topEle.getAttribute("class").includes("hasMore-"));
            restartTimer(200);
          }
        }
        else{
          restartTimer(25);
        }
      };

      var restartTimer = function(delay){
        observerTimer = DOM.setTimer(onTimerFinished, delay);
      };

      onTimerFinished();
      window.DHT_ON_UNLOAD.push(() => window.clearInterval(observerTimer));
    },

    /*
     * Returns internal React state object of an element.
     */
    getReactProps: function(ele){
      var key = Object.keys(ele || {}).find(key => key.startsWith("__reactInternalInstance"));
      return key ? ele[key].memoizedProps : null;
    },

    /*
     * Returns an object containing the selected server name, selected channel name and ID, and the object type.
     * For types DM and GROUP, the server and channel names are identical.
     * For SERVER type, the channel has to be in view, otherwise Discord unloads it.
     */
    getSelectedChannel: function(){
      try{
        var obj;
        var channelListEle = document.querySelector("[class|='privateChannels']");

        if (channelListEle){
          var channel = DOM.queryReactClass("selected", channelListEle);

          if (!channel){
            return null;
          }

          var linkSplit = channel.querySelector("a[href*='/@me/']").href.split("/");
          var link = linkSplit[linkSplit.length-1];

          if (!(/^\d+$/.test(link))){
            return null;
          }

          var name = Array.prototype.find.call(channel.querySelector("[class|='name']").childNodes, node => node.nodeType === Node.TEXT_NODE).nodeValue;

          obj = {
            "server": name,
            "channel": name,
            "id": link,
            "type": !!DOM.queryReactClass("status", channel) ? "DM" : "GROUP"
          };
        }
        else{
          channelListEle = document.querySelector("[class|='channels']");

          var channel = channelListEle.querySelector("[class|='wrapperSelectedText']").parentElement;
          var props = DISCORD.getReactProps(channel);

          if (!props){
            return null;
          }

          var channelObj = props.children.props.channel;

          if (!channelObj){
            return null;
          }

          obj = {
            "server": channelListEle.querySelector("header > span").innerHTML,
            "channel": channelObj.name,
            "id": channelObj.id,
            "type": "SERVER"
          };
        }

        return obj.channel.length === 0 ? null : obj;
      }catch(e){
        return null;
      }
    },

    /*
     * Returns an array containing currently loaded messages.
     */
    getMessages: function(){
      var props = DISCORD.getReactProps(DOM.queryReactClass("messages"));
      var array = props && props.children.find(ele => ele && ele.length);
      var messages = [];

      if (array){
        for(let obj of array){
          if (obj.props.messages){
            Array.prototype.push.apply(messages, obj.props.messages);
          }
        }
      }

      return messages;
    },

    /*
     * Returns true if the message view is visible.
     */
    isInMessageView: () => !!DOM.queryReactClass("messages"),

    /*
     * Returns true if there are more messages available or if they're still loading.
     */
    hasMoreMessages: function(){
      let classes = getTopMessageViewElement().getAttribute("class");
      return classes.includes("hasMore-") || classes.includes("loadingMore-");
    },

    /*
     * Forces the message view to load older messages by scrolling all the way up.
     */
    loadOlderMessages: function(){
      let view = DOM.queryReactClass("messages");
      view.scrollTop = view.scrollHeight/2;
      view.scrollTop = 0;
    },

    /*
     * Selects the next text channel and returns true, otherwise returns false if there are no more channels.
     */
    selectNextTextChannel: function(){
      var dms = document.querySelector("[class|='privateChannels']");

      if (dms){
        var nextChannel = DOM.queryReactClass("selected", dms).nextElementSibling;
        var nextLink = nextChannel && nextChannel.querySelector("a[href*='/@me/']");

        if (!nextChannel || !nextLink || !nextChannel.getAttribute("class").includes("channel-")){
          return false;
        }
        else{
          nextLink.click();
          return true;
        }
      }
      else{
        var isValidChannel = ele => ele.childElementCount > 0 && /wrapper([a-zA-Z]+?)Text/.test(ele.children[0].className);
        var allChannels = Array.prototype.filter.call(document.querySelector("[class|='channels']").querySelectorAll("[class|='containerDefault']"), isValidChannel);

        var nextChannel = null;

        for(var index = 0; index < allChannels.length-1; index++){
          if (allChannels[index].children[0].className.includes("wrapperSelectedText")){
            nextChannel = allChannels[index+1];
            break;
          }
        }

        if (nextChannel === null){
          return false;
        }
        else{
          nextChannel.children[0].click();
          return true;
        }
      }
    }
  };
})();
<|endoftext|>


<commit_before>(function () {

  initMobileMenu()
  if (PAGE_TYPE) {
    initVersionSelect()
    initSubHeaders()
    initApiSpecLinks()
    initLocationHashFuzzyMatching()
  }

  function initApiSpecLinks () {
    var apiContent = document.querySelector('.content.api')
    if (apiContent) {
      var apiTitles = [].slice.call(apiContent.querySelectorAll('h3'))
      apiTitles.forEach(function (titleNode) {
        var ulNode = titleNode.parentNode.nextSibling
        if (ulNode.tagName !== 'UL') {
          ulNode = ulNode.nextSibling
        }
        if (ulNode.tagName === 'UL') {
          var specNode = document.createElement('li')
          var specLink = createSourceSearchPath(titleNode.textContent)
          specNode.innerHTML = 'Source'
          ulNode.appendChild(specNode)
        }
      })
    }

    function createSourceSearchPath (query) {
      return 'https://github.com/search?utf8=%E2%9C%93&q=repo%3Avuejs%2Fvue+extension%3Ajs+' + encodeURIComponent(query) + '+&type=Code'
    }
  }

  function initLocationHashFuzzyMatching () {
    var hash = window.location.hash
    if (!hash) return
    var hashTarget = document.getElementById(hash)
    if (!hashTarget) {
      var normalizedHash = normalizeHash(hash)
      var possibleHashes = [].slice.call(document.querySelectorAll('[id]'))
        .map(function (el) { return el.id })
      possibleHashes.sort(function (hashA, hashB) {
        var distanceA = levenshteinDistance(normalizedHash, normalizeHash(hashA))
        var distanceB = levenshteinDistance(normalizedHash, normalizeHash(hashB))
        if (distanceA < distanceB) return -1
        if (distanceA > distanceB) return 1
        return 0
      })
      window.location.hash = possibleHashes[0]
    }

    function normalizeHash (rawHash) {
      return rawHash
        .toLowerCase()
        .replace(/\-(?:deprecated|removed|replaced|changed|obsolete)$/, '')
    }

    function levenshteinDistance (a, b) {
      var m = []
      if (!(a && b)) return (b || a).length
      for (let i = 0; i <= b.length; m[i] = [i++]) {}
      for (let j = 0; j <= a.length; m[0][j] = j++) {}
      for (let i = 1; i <= b.length; i++) {
        for (let j = 1; j <= a.length; j++) {
          m[i][j] = b.charAt(i - 1) === a.charAt(j - 1)
            ? m[i - 1][j - 1]
            : m[i][j] = Math.min(
              m[i - 1][j - 1] + 1,
              Math.min(m[i][j - 1] + 1, m[i - 1][j] + 1))
        }
      }
      return m[b.length][a.length]
    }
  }

  /**
   * Mobile burger menu button for toggling sidebar
   */

  function initMobileMenu () {
    var mobileBar = document.getElementById('mobile-bar')
    var sidebar = document.querySelector('.sidebar')
    var menuButton = mobileBar.querySelector('.menu-button')

    menuButton.addEventListener('click', function () {
      sidebar.classList.toggle('open')
    })

    document.body.addEventListener('click', function (e) {
      if (e.target !== menuButton && !sidebar.contains(e.target)) {
        sidebar.classList.remove('open')
      }
    })
  }

  /**
   * Doc version select
   */

  function initVersionSelect () {
    // version select
    var versionSelect = document.querySelector('.version-select')
    versionSelect && versionSelect.addEventListener('change', function (e) {
      var version = e.target.value
      var section = window.location.pathname.match(/\/v\d\/(\w+?)\//)[1]
      if (version === 'SELF') { return }
      window.location.assign('http://' + (version ? version + '-' : '') + 'jp.vuejs.org/' + section + '/')
    })
  }

  /**
   * Sub headers in sidebar
   */

  function initSubHeaders () {
    var each = [].forEach
    var main = document.getElementById('main')
    var header = document.getElementById('header')
    var sidebar = document.querySelector('.sidebar')
    var content = document.querySelector('.content')

    // build sidebar
    var currentPageAnchor = sidebar.querySelector('.sidebar-link.current')
    var isAPI = document.querySelector('.content').classList.contains('api')
    if (currentPageAnchor || isAPI) {
      var allHeaders = []
      var sectionContainer
      if (isAPI) {
        sectionContainer = document.querySelector('.menu-root')
      } else {
        sectionContainer = document.createElement('ul')
        sectionContainer.className = 'menu-sub'
        currentPageAnchor.parentNode.appendChild(sectionContainer)
      }
      var headers = content.querySelectorAll('h2')
      if (headers.length) {
        each.call(headers, function (h) {
          sectionContainer.appendChild(makeLink(h))
          var h3s = collectH3s(h)
          allHeaders.push(h)
          allHeaders.push.apply(allHeaders, h3s)
          if (h3s.length) {
            sectionContainer.appendChild(makeSubLinks(h3s, isAPI))
          }
        })
      } else {
        headers = content.querySelectorAll('h3')
        each.call(headers, function (h) {
          sectionContainer.appendChild(makeLink(h))
          allHeaders.push(h)
        })
      }

      var animating = false
      sectionContainer.addEventListener('click', function (e) {
        e.preventDefault()
        if (e.target.classList.contains('section-link')) {
          sidebar.classList.remove('open')
          setActive(e.target)
          animating = true
          setTimeout(function () {
            animating = false
          }, 400)
        }
      }, true)

      // make links clickable
      allHeaders.forEach(makeHeaderClickable)

      // init smooth scroll
      smoothScroll.init({
        speed: 400,
        offset: 0
      })
    }

    var hoveredOverSidebar = false
    sidebar.addEventListener('mouseover', function () {
      hoveredOverSidebar = true
    })
    sidebar.addEventListener('mouseleave', function () {
      hoveredOverSidebar = false
    })

    // listen for scroll event to do positioning & highlights
    window.addEventListener('scroll', updateSidebar)
    window.addEventListener('resize', updateSidebar)

    function updateSidebar () {
      var doc = document.documentElement
      var top = doc && doc.scrollTop || document.body.scrollTop
      if (animating || !allHeaders) return
      var last
      for (var i = 0; i < allHeaders.length; i++) {
        var link = allHeaders[i]
        if (link.offsetTop > top) {
          if (!last) last = link
          break
        } else {
          last = link
        }
      }
      if (last)
      setActive(last.id, !hoveredOverSidebar)
    }

    function makeLink (h) {
      var link = document.createElement('li')
      var text = h.textContent.replace(/\(.*\)$/, '')
      link.innerHTML =
        '' +
          text +
        ''
      return link
    }

    function collectH3s (h) {
      var h3s = []
      var next = h.nextSibling
      while (next && next.tagName !== 'H2') {
        if (next.tagName === 'H3') {
          h3s.push(next)
        }
        next = next.nextSibling
      }
      return h3s
    }

    function makeSubLinks (h3s, small) {
      var container = document.createElement('ul')
      if (small) {
        container.className = 'menu-sub'
      }
      h3s.forEach(function (h) {
        container.appendChild(makeLink(h))
      })
      return container
    }

    function setActive (id, shouldScrollIntoView) {
      var previousActive = sidebar.querySelector('.section-link.active')
      var currentActive = typeof id === 'string'
        ? sidebar.querySelector('.section-link[href="#' + id + '"]')
        : id
      if (currentActive !== previousActive) {
        if (previousActive) previousActive.classList.remove('active')
        currentActive.classList.add('active')
        if (shouldScrollIntoView) {
          var currentPageOffset = currentPageAnchor
            ? currentPageAnchor.offsetTop - 8
            : 0
          var currentActiveOffset = currentActive.offsetTop + currentActive.parentNode.clientHeight
          var sidebarHeight = sidebar.clientHeight
          var currentActiveIsInView = (
            currentActive.offsetTop >= sidebar.scrollTop &&
            currentActiveOffset <= sidebar.scrollTop + sidebarHeight
          )
          var linkNotFurtherThanSidebarHeight = currentActiveOffset - currentPageOffset < sidebarHeight
          var newScrollTop = currentActiveIsInView
            ? sidebar.scrollTop
            : linkNotFurtherThanSidebarHeight
              ? currentPageOffset
              : currentActiveOffset - sidebarHeight
          sidebar.scrollTop = newScrollTop
        }
      }
    }

    function makeHeaderClickable (link) {
      var wrapper = document.createElement('a')
      wrapper.href = '#' + link.id
      wrapper.setAttribute('data-scroll', '')
      link.parentNode.insertBefore(wrapper, link)
      wrapper.appendChild(link)
    }
  }
})()
<commit_msg>fix let usage in common.js<commit_after>(function () {

  initMobileMenu()
  if (PAGE_TYPE) {
    initVersionSelect()
    initSubHeaders()
    initApiSpecLinks()
    initLocationHashFuzzyMatching()
  }

  function initApiSpecLinks () {
    var apiContent = document.querySelector('.content.api')
    if (apiContent) {
      var apiTitles = [].slice.call(apiContent.querySelectorAll('h3'))
      apiTitles.forEach(function (titleNode) {
        var ulNode = titleNode.parentNode.nextSibling
        if (ulNode.tagName !== 'UL') {
          ulNode = ulNode.nextSibling
        }
        if (ulNode.tagName === 'UL') {
          var specNode = document.createElement('li')
          var specLink = createSourceSearchPath(titleNode.textContent)
          specNode.innerHTML = 'Source'
          ulNode.appendChild(specNode)
        }
      })
    }

    function createSourceSearchPath (query) {
      return 'https://github.com/search?utf8=%E2%9C%93&q=repo%3Avuejs%2Fvue+extension%3Ajs+' + encodeURIComponent(query) + '+&type=Code'
    }
  }

  function initLocationHashFuzzyMatching () {
    var hash = window.location.hash
    if (!hash) return
    var hashTarget = document.getElementById(hash)
    if (!hashTarget) {
      var normalizedHash = normalizeHash(hash)
      var possibleHashes = [].slice.call(document.querySelectorAll('[id]'))
        .map(function (el) { return el.id })
      possibleHashes.sort(function (hashA, hashB) {
        var distanceA = levenshteinDistance(normalizedHash, normalizeHash(hashA))
        var distanceB = levenshteinDistance(normalizedHash, normalizeHash(hashB))
        if (distanceA < distanceB) return -1
        if (distanceA > distanceB) return 1
        return 0
      })
      window.location.hash = possibleHashes[0]
    }

    function normalizeHash (rawHash) {
      return rawHash
        .toLowerCase()
        .replace(/\-(?:deprecated|removed|replaced|changed|obsolete)$/, '')
    }

    function levenshteinDistance (a, b) {
      var m = []
      if (!(a && b)) return (b || a).length
      for (var i = 0; i <= b.length; m[i] = [i++]) {}
      for (var j = 0; j <= a.length; m[0][j] = j++) {}
      for (var i = 1; i <= b.length; i++) {
        for (var j = 1; j <= a.length; j++) {
          m[i][j] = b.charAt(i - 1) === a.charAt(j - 1)
            ? m[i - 1][j - 1]
            : m[i][j] = Math.min(
              m[i - 1][j - 1] + 1,
              Math.min(m[i][j - 1] + 1, m[i - 1][j] + 1))
        }
      }
      return m[b.length][a.length]
    }
  }

  /**
   * Mobile burger menu button for toggling sidebar
   */

  function initMobileMenu () {
    var mobileBar = document.getElementById('mobile-bar')
    var sidebar = document.querySelector('.sidebar')
    var menuButton = mobileBar.querySelector('.menu-button')

    menuButton.addEventListener('click', function () {
      sidebar.classList.toggle('open')
    })

    document.body.addEventListener('click', function (e) {
      if (e.target !== menuButton && !sidebar.contains(e.target)) {
        sidebar.classList.remove('open')
      }
    })
  }

  /**
   * Doc version select
   */

  function initVersionSelect () {
    // version select
    var versionSelect = document.querySelector('.version-select')
    versionSelect && versionSelect.addEventListener('change', function (e) {
      var version = e.target.value
      var section = window.location.pathname.match(/\/v\d\/(\w+?)\//)[1]
      if (version === 'SELF') { return }
      window.location.assign('http://' + (version ? version + '-' : '') + 'jp.vuejs.org/' + section + '/')
    })
  }

  /**
   * Sub headers in sidebar
   */

  function initSubHeaders () {
    var each = [].forEach
    var main = document.getElementById('main')
    var header = document.getElementById('header')
    var sidebar = document.querySelector('.sidebar')
    var content = document.querySelector('.content')

    // build sidebar
    var currentPageAnchor = sidebar.querySelector('.sidebar-link.current')
    var isAPI = document.querySelector('.content').classList.contains('api')
    if (currentPageAnchor || isAPI) {
      var allHeaders = []
      var sectionContainer
      if (isAPI) {
        sectionContainer = document.querySelector('.menu-root')
      } else {
        sectionContainer = document.createElement('ul')
        sectionContainer.className = 'menu-sub'
        currentPageAnchor.parentNode.appendChild(sectionContainer)
      }
      var headers = content.querySelectorAll('h2')
      if (headers.length) {
        each.call(headers, function (h) {
          sectionContainer.appendChild(makeLink(h))
          var h3s = collectH3s(h)
          allHeaders.push(h)
          allHeaders.push.apply(allHeaders, h3s)
          if (h3s.length) {
            sectionContainer.appendChild(makeSubLinks(h3s, isAPI))
          }
        })
      } else {
        headers = content.querySelectorAll('h3')
        each.call(headers, function (h) {
          sectionContainer.appendChild(makeLink(h))
          allHeaders.push(h)
        })
      }

      var animating = false
      sectionContainer.addEventListener('click', function (e) {
        e.preventDefault()
        if (e.target.classList.contains('section-link')) {
          sidebar.classList.remove('open')
          setActive(e.target)
          animating = true
          setTimeout(function () {
            animating = false
          }, 400)
        }
      }, true)

      // make links clickable
      allHeaders.forEach(makeHeaderClickable)

      // init smooth scroll
      smoothScroll.init({
        speed: 400,
        offset: 0
      })
    }

    var hoveredOverSidebar = false
    sidebar.addEventListener('mouseover', function () {
      hoveredOverSidebar = true
    })
    sidebar.addEventListener('mouseleave', function () {
      hoveredOverSidebar = false
    })

    // listen for scroll event to do positioning & highlights
    window.addEventListener('scroll', updateSidebar)
    window.addEventListener('resize', updateSidebar)

    function updateSidebar () {
      var doc = document.documentElement
      var top = doc && doc.scrollTop || document.body.scrollTop
      if (animating || !allHeaders) return
      var last
      for (var i = 0; i < allHeaders.length; i++) {
        var link = allHeaders[i]
        if (link.offsetTop > top) {
          if (!last) last = link
          break
        } else {
          last = link
        }
      }
      if (last)
      setActive(last.id, !hoveredOverSidebar)
    }

    function makeLink (h) {
      var link = document.createElement('li')
      var text = h.textContent.replace(/\(.*\)$/, '')
      link.innerHTML =
        '' +
          text +
        ''
      return link
    }

    function collectH3s (h) {
      var h3s = []
      var next = h.nextSibling
      while (next && next.tagName !== 'H2') {
        if (next.tagName === 'H3') {
          h3s.push(next)
        }
        next = next.nextSibling
      }
      return h3s
    }

    function makeSubLinks (h3s, small) {
      var container = document.createElement('ul')
      if (small) {
        container.className = 'menu-sub'
      }
      h3s.forEach(function (h) {
        container.appendChild(makeLink(h))
      })
      return container
    }

    function setActive (id, shouldScrollIntoView) {
      var previousActive = sidebar.querySelector('.section-link.active')
      var currentActive = typeof id === 'string'
        ? sidebar.querySelector('.section-link[href="#' + id + '"]')
        : id
      if (currentActive !== previousActive) {
        if (previousActive) previousActive.classList.remove('active')
        currentActive.classList.add('active')
        if (shouldScrollIntoView) {
          var currentPageOffset = currentPageAnchor
            ? currentPageAnchor.offsetTop - 8
            : 0
          var currentActiveOffset = currentActive.offsetTop + currentActive.parentNode.clientHeight
          var sidebarHeight = sidebar.clientHeight
          var currentActiveIsInView = (
            currentActive.offsetTop >= sidebar.scrollTop &&
            currentActiveOffset <= sidebar.scrollTop + sidebarHeight
          )
          var linkNotFurtherThanSidebarHeight = currentActiveOffset - currentPageOffset < sidebarHeight
          var newScrollTop = currentActiveIsInView
            ? sidebar.scrollTop
            : linkNotFurtherThanSidebarHeight
              ? currentPageOffset
              : currentActiveOffset - sidebarHeight
          sidebar.scrollTop = newScrollTop
        }
      }
    }

    function makeHeaderClickable (link) {
      var wrapper = document.createElement('a')
      wrapper.href = '#' + link.id
      wrapper.setAttribute('data-scroll', '')
      link.parentNode.insertBefore(wrapper, link)
      wrapper.appendChild(link)
    }
  }
})()
<|endoftext|>