<commit_before>const momentTZ = require("moment-timezone");
require("moment-duration-format");
// const {inspect} = require('util');
const Command = require("../base/Command");

class Shardtimes extends Command {
    constructor(Bot) {
        super(Bot, {
            name: "shardtimes",
            aliases: ["shard", "st", "payout", "po"],
            guildOnly: true,
            category: "Misc",
            permissions: ["EMBED_LINKS"],
            flags: {
                "ships": {
                    aliases: ["ship", "s"]
                }
            },
            subArgs: {
                "timeuntil": {
                    aliases: ["tu"]
                }
            }
        });
    }

    async run(Bot, message, [action, userID, timezone, ...flag], options) {
        const level = options.level;
        // Shard ID will be guild.id-channel.id
        const shardID = `${message.guild.id}-${message.channel.id}`;

        const exists = await Bot.database.models.shardtimes.findOne({where: {id: shardID}})
            .then(token => token != null)
            .then(isUnique => isUnique);

        let shardTimes = {};

        if (!exists) {
            await Bot.database.models.shardtimes.create({
                id: shardID,
                times: shardTimes
            });
        } else {
            const tempT = await Bot.database.models.shardtimes.findOne({where: {id: shardID}});
            shardTimes = tempT.dataValues.times;
        }

        let timeToAdd = 18;
        if (options.flags.ships) {
            timeToAdd = 19;
        }

        if (action === "add") {
            // If it's an admin, let them register other users, else let em register themselves
            // To add someone ;shardinfo <me|@mention|discordID> <timezone> [flag/emoji]
            let type = "id", zoneType = "zone";
            let tempZone = timezone ? timezone : null;
            if (userID === "me") {
                userID = message.author.id;
            } else if (userID.match(/\d{17,18}/)) {
                userID = userID.replace(/[^\d]*/g, "");
            } else {
                type = "name";
            }
            if (userID !== message.author.id && userID !== message.author.username && level < 3) {
                return super.error(message, message.language.get("COMMAND_SHARDTIMES_REM_MISSING_PERMS"));
            }
            if (!options.subArgs.timeuntil) {
                if (!timezone) {
                    // Grumble that they need a timezone, then give the wiki list
                    return super.error(message, message.language.get("COMMAND_SHARDTIMES_MISSING_TIMEZONE"));
                } else {
                    if (!momentTZ.tz.zone(timezone)) { // Valid time zone?
                        const match = timezone.match(/([+-])(2[0-3]|[01]{0,1}[0-9]):([0-5][0-9])/);
                        if (match) {
                            // It's a UTC +/- zone
                            zoneType = "utc";
                            timezone = parseInt(`${match[1]}${parseInt(match[2] * 60) + parseInt(match[3])}`);
                        } else {
                            // Grumble that it's an invalid tz
                            return super.error(message, message.language.get("COMMAND_SHARDTIMES_INVALID_TIMEZONE"));
                        }
                    }
                }
            } else {
                flag = timezone ? [timezone] : [];
                zoneType = "hhmm";
                const timeTil = options.subArgs.timeuntil;
                const match = timeTil.match(/(2[0-3]|[01]{0,1}[0-9]):([0-5][0-9])/);
                if (match) {
                    // It's a valid time until payout
                    const [hour, minute] = timeTil.split(":");
                    const [tempH, tempM] = momentTZ().add(hour, "h").add(minute, "m").format("HH:mm").split(":").map(t => parseInt(t));
                    const totalMin = (tempH * 60) + tempM;
                    const rounded = Math.round(totalMin / 15) * 15;
                    timezone = `${Math.floor(rounded / 60)}:${(rounded % 60).toString().padEnd(2, "0")}`;
                    tempZone = timezone + " UTC";
                } else {
                    return super.error(message, message.language.get("COMMAND_SHARDTIMES_INVALID_TIME_TIL"));
                }
            }
            if (flag.length > 0) {
                flag = flag[0];
                if (flag.match(/<:.+:\d+>/)) {
                    flag = flag.replace(/<:.*:/, "").replace(/>$/, "");
                }
            } else {
                flag = "";
            }
            let tempUser = null;
            if (exists && shardTimes[`${userID}`]) {
                tempUser = shardTimes[`${userID}`];
                if (tempUser.zoneType) {
                    if (tempUser.zoneType === "utc") {
                        const sign = tempUser.timezone >= 0 ? "+" : "-";
                        const num = Math.abs(tempUser.timezone);
                        const hour = Math.floor(num / 60);
                        let min = (num % 60).toString();
                        if (min.length === 1) min = "0" + min;
                        tempUser.tempZone = sign + hour + ":" + min;
                    } else if (tempUser.zoneType === "hhmm") {
                        tempUser.tempZone = tempUser.timezone + " UTC";
                    } else if (tempUser.zoneType === "zone") {
                        tempUser.tempZone = tempUser.timezone;
                    }
                } else {
                    tempUser.tempZone = tempUser.timezone;
                }
            }
            shardTimes[`${userID}`] = {
                "type": type,
                "zoneType": zoneType,
                "timezone": timezone,
                "flag": flag
            };
            await Bot.database.models.shardtimes.update({times: shardTimes}, {where: {id: shardID}})
                .then(() => {
                    if (tempUser) {
                        return message.channel.send(message.language.get("COMMAND_SHARDTIMES_USER_MOVED", tempUser.tempZone, tempZone));
                    }
                    return message.channel.send(message.language.get("COMMAND_SHARDTIMES_USER_ADDED"));
                })
                .catch(() => {
                    return super.error(message, message.language.get("COMMAND_SHARDTIMES_USER_NOT_ADDED"));
                });
        } else if (action === "remove" || action === "rem") {
            // Get the json object, remove the user if available, then resave if it changed
            if (userID !== message.author.id && userID !== message.author.username && level < 3) {
                return super.error(message, message.language.get("COMMAND_SHARDTIMES_REM_MISSING_PERMS"));
            }
            if (userID === "me") {
                userID = message.author.id;
            } else if (userID.match(/\d{17,18}/)) {
                // If they are trying to remove someone else and they don't have the right perms, stop em
                userID = userID.replace(/[\\|<|@|!]*(\d{17,18})[>]*/g,"$1");
            }
            if (shardTimes.hasOwnProperty(userID)) {
                delete shardTimes[userID];
                await Bot.database.models.shardtimes.update({times: shardTimes}, {where: {id: shardID}})
                    .then(() => {
                        return message.channel.send(message.language.get("COMMAND_SHARDTIMES_REM_SUCCESS"));
                    })
                    .catch(() => {
                        return super.error(message, message.language.get("COMMAND_SHARDTIMES_REM_FAIL"));
                    });
            } else {
                return super.error(message, message.language.get("COMMAND_SHARDTIMES_REM_MISSING"));
            }
        } else if (action === "copy") {  // ;shardtimes copy destChannel
            // Make sure the person has the correct perms to copy it (admin/ mod)
            if (level < 3) {  // Permlevel 3 is the adminRole of the server, so anyone under that shouldn"t be able to do this
                return super.error(message, message.language.get("COMMAND_EVENT_INVALID_PERMS"));
            }
            // Make sure there are times to copy from
            if (!Object.keys(shardTimes).length) {
                return super.error(message, message.language.get("COMMAND_SHARDTIMES_COPY_NO_SOURCE"));
            }
            // Check and make sure the destination channel exists/ the bot has permissions to see/ send there
            let destChan = message.guild.channels.find(c => c.name === userID);
            if (!destChan) {
                destChan = message.guild.channels.get(userID.replace(/[^0-9]/g, ""));
                if (!destChan) {
                    return super.error(message, message.language.get("COMMAND_SHARDTIMES_COPY_NO_DEST", userID));
                }
            }
            if (!destChan.permissionsFor(message.guild.me).has(["SEND_MESSAGES", "VIEW_CHANNEL"])) {
                return super.error(message, message.language.get("COMMAND_SHARDTIMES_COPY_NO_PERMS", destChan.id));
            }

            if (message.channel.id === destChan.id) {
                return super.error(message, message.language.get("COMMAND_SHARDTIMES_COPY_SAME_CHAN"));
            }

            const destShardID = `${message.guild.id}-${destChan.id}`;

            const destExists = await Bot.database.models.shardtimes.findOne({where: {id: destShardID}})
                .then(token => token != null)
                .then(isUnique => isUnique);

            if (!destExists) {
                // If there's no shard info in the destination channel
                await Bot.database.models.shardtimes.create({ id: destShardID, times: shardTimes })
                    .then(() => {
                        return message.channel.send(message.language.get("COMMAND_SHARDTIMES_COPY_SUCCESS", destChan.id));
                    })
                    .catch((err) => {
                        return super.error(message, err.message);
                    });
            } else {
                const destHasTimes = await Bot.database.models.shardtimes.findOne({where: {id: destShardID}})
                    .then(times => Object.keys(times.dataValues.times).length)
                    .then(isLen => isLen);
                if (destHasTimes) {
                    // Of if there is shard info there with listings
                    return super.error(message, message.language.get("COMMAND_SHARDTIMES_COPY_DEST_FULL"));
                } else {
                    // Or if there is shard info there, but no listings
                    await Bot.database.models.shardtimes.update({times: shardTimes}, {where: {id: destShardID}})
                        .then(() => {
                            return message.channel.send(message.language.get("COMMAND_SHARDTIMES_COPY_SUCCESS", destChan.id));
                        })
                        .catch(() => {
                            return super.error(message, message.language.get("COMMAND_SHARDTIMES_COPY_BROKE"));
                        });
                }
            }
            return;
        } else {
            // View the shard table
            const shardOut = {};
            Object.keys(shardTimes).forEach(user => {
                const diff = timeTil(shardTimes[user].timezone, timeToAdd, (shardTimes[user].zoneType ? shardTimes[user].zoneType : "zone"));
                if (shardOut.hasOwnProperty(diff)) {
                    shardOut[diff].push(user);
                } else {
                    shardOut[diff] = [user];
                }
            });

            const sortedShardTimes = Object.keys(shardOut).sort((a, b) => momentTZ(a, "HH:mm").diff(momentTZ(b, "HH:mm")));

            const fields = [];
            sortedShardTimes.forEach(time => {
                const times = [];
                shardOut[time].forEach(user => {
                    let userFlag = message.client.emojis.get(shardTimes[user].flag);
                    if (!userFlag) {
                        userFlag = shardTimes[user].flag;
                    }
                    const maxLen = 20;
                    let uName = "";
                    if (!shardTimes[user].type || shardTimes[user].type === "id") {
                        const thisUser = message.guild.members.get(user);
                        const userName = thisUser ? `${thisUser.displayName}` : user;
                        uName = "**" + (userName.length > maxLen ? userName.substring(0, maxLen) : userName) + "**";
                    } else {
                        // Type is name, don't try looking it up
                        const userName = user;
                        uName = userName.length > maxLen ? userName.substring(0, maxLen) : userName;
                    }
                    times.push({
                        flag: shardTimes[user].flag != "" ? userFlag : "",
                        name: uName
                    });
                });
                const sortedTimes = times.sort((a, b) => a.name.toLowerCase() > b.name.toLowerCase() ? 1 : 0).map(t => `${t.flag}${t.name}`);
                let joiner = " - ";
                if (message.guildSettings.shardtimeVertical) {
                    joiner = "\n";
                }
                fields.push({
                    name: time,
                    value: sortedTimes.join(joiner)
                });
            });
            return message.channel.send({
                embed: {
                    "author": {
                        "name": message.language.get("COMMAND_SHARDTIMES_SHARD_HEADER")
                    },
                    "fields": fields
                }
            });
        }

        function timeTil(zone, timeToAdd, type) {
            let targetTime;
            if (type === "zone") {
                if (momentTZ.tz(zone).unix() < momentTZ.tz(zone).startOf("day").add(timeToAdd, "h").unix()) {
                    // If it's later today
                    targetTime = momentTZ.tz(zone).startOf("day").add(timeToAdd, "h");
                } else {
                    // If it's already passed for the day
                    targetTime = momentTZ.tz(zone).startOf("day").add(1, "d").add(timeToAdd, "h");
                }
            } else if (type === "hhmm") {
                if (momentTZ(zone, "HH:mm").unix() < momentTZ().unix()) {
                    // It's already passed
                    return momentTZ.duration(momentTZ(zone, "HH:mm").add(1, "d").diff(momentTZ())).format("HH:mm", { trim: false });
                }
                return momentTZ.duration(momentTZ(zone, "HH:mm").diff(momentTZ())).format("HH:mm", { trim: false });
            } else {
                // It's utc +/- format
                if (momentTZ().utcOffset(zone).unix() < momentTZ().utcOffset(zone).startOf("day").add(timeToAdd, "h").unix()) {
                    targetTime = momentTZ().utcOffset(zone).startOf("day").add(timeToAdd, "h");
                } else {
                    targetTime = momentTZ().utcOffset(zone).startOf("day").add(1, "d").add(timeToAdd, "h");
                }
                return momentTZ.duration(targetTime.diff(momentTZ().utcOffset(zone))).format("HH:mm", { trim: false });
            }
            return momentTZ.duration(targetTime.diff(momentTZ.tz(zone))).format("HH:mm", { trim: false });
        }
    }
}

module.exports = Shardtimes;

<commit_msg>Update shardtimes to force use utc time<commit_after>const momentTZ = require("moment-timezone");
require("moment-duration-format");
// const {inspect} = require('util');
const Command = require("../base/Command");

class Shardtimes extends Command {
    constructor(Bot) {
        super(Bot, {
            name: "shardtimes",
            aliases: ["shard", "st", "payout", "po"],
            guildOnly: true,
            category: "Misc",
            permissions: ["EMBED_LINKS"],
            flags: {
                "ships": {
                    aliases: ["ship", "s"]
                }
            },
            subArgs: {
                "timeuntil": {
                    aliases: ["tu"]
                }
            }
        });
    }

    async run(Bot, message, [action, userID, timezone, ...flag], options) {
        const level = options.level;
        // Shard ID will be guild.id-channel.id
        const shardID = `${message.guild.id}-${message.channel.id}`;

        const exists = await Bot.database.models.shardtimes.findOne({where: {id: shardID}})
            .then(token => token != null)
            .then(isUnique => isUnique);

        let shardTimes = {};

        if (!exists) {
            await Bot.database.models.shardtimes.create({
                id: shardID,
                times: shardTimes
            });
        } else {
            const tempT = await Bot.database.models.shardtimes.findOne({where: {id: shardID}});
            shardTimes = tempT.dataValues.times;
        }

        let timeToAdd = 18;
        if (options.flags.ships) {
            timeToAdd = 19;
        }

        if (action === "add") {
            // If it's an admin, let them register other users, else let em register themselves
            // To add someone ;shardinfo <me|@mention|discordID> <timezone> [flag/emoji]
            let type = "id", zoneType = "zone";
            let tempZone = timezone ? timezone : null;
            if (userID === "me") {
                userID = message.author.id;
            } else if (userID.match(/\d{17,18}/)) {
                userID = userID.replace(/[^\d]*/g, "");
            } else {
                type = "name";
            }
            if (userID !== message.author.id && userID !== message.author.username && level < 3) {
                return super.error(message, message.language.get("COMMAND_SHARDTIMES_REM_MISSING_PERMS"));
            }
            if (!options.subArgs.timeuntil) {
                if (!timezone) {
                    // Grumble that they need a timezone, then give the wiki list
                    return super.error(message, message.language.get("COMMAND_SHARDTIMES_MISSING_TIMEZONE"));
                } else {
                    if (!momentTZ.tz.zone(timezone)) { // Valid time zone?
                        const match = timezone.match(/([+-])(2[0-3]|[01]{0,1}[0-9]):([0-5][0-9])/);
                        if (match) {
                            // It's a UTC +/- zone
                            zoneType = "utc";
                            timezone = parseInt(`${match[1]}${parseInt(match[2] * 60) + parseInt(match[3])}`);
                        } else {
                            // Grumble that it's an invalid tz
                            return super.error(message, message.language.get("COMMAND_SHARDTIMES_INVALID_TIMEZONE"));
                        }
                    }
                }
            } else {
                flag = timezone ? [timezone] : [];
                zoneType = "hhmm";
                const timeTil = options.subArgs.timeuntil;
                const match = timeTil.match(/(2[0-3]|[01]{0,1}[0-9]):([0-5][0-9])/);
                if (match) {
                    // It's a valid time until payout
                    const [hour, minute] = timeTil.split(":");
                    const [tempH, tempM] = momentTZ.tz("UTC").add(hour, "h").add(minute, "m").format("HH:mm").split(":").map(t => parseInt(t));
                    const totalMin = (tempH * 60) + tempM;
                    const rounded = Math.round(totalMin / 15) * 15;
                    timezone = `${Math.floor(rounded / 60)}:${(rounded % 60).toString().padEnd(2, "0")}`;
                    tempZone = timezone + " UTC";
                } else {
                    return super.error(message, message.language.get("COMMAND_SHARDTIMES_INVALID_TIME_TIL"));
                }
            }
            if (flag.length > 0) {
                flag = flag[0];
                if (flag.match(/<:.+:\d+>/)) {
                    flag = flag.replace(/<:.*:/, "").replace(/>$/, "");
                }
            } else {
                flag = "";
            }
            let tempUser = null;
            if (exists && shardTimes[`${userID}`]) {
                tempUser = shardTimes[`${userID}`];
                if (tempUser.zoneType) {
                    if (tempUser.zoneType === "utc") {
                        const sign = tempUser.timezone >= 0 ? "+" : "-";
                        const num = Math.abs(tempUser.timezone);
                        const hour = Math.floor(num / 60);
                        let min = (num % 60).toString();
                        if (min.length === 1) min = "0" + min;
                        tempUser.tempZone = sign + hour + ":" + min;
                    } else if (tempUser.zoneType === "hhmm") {
                        tempUser.tempZone = tempUser.timezone + " UTC";
                    } else if (tempUser.zoneType === "zone") {
                        tempUser.tempZone = tempUser.timezone;
                    }
                } else {
                    tempUser.tempZone = tempUser.timezone;
                }
            }
            shardTimes[`${userID}`] = {
                "type": type,
                "zoneType": zoneType,
                "timezone": timezone,
                "flag": flag
            };
            await Bot.database.models.shardtimes.update({times: shardTimes}, {where: {id: shardID}})
                .then(() => {
                    if (tempUser) {
                        return message.channel.send(message.language.get("COMMAND_SHARDTIMES_USER_MOVED", tempUser.tempZone, tempZone));
                    }
                    return message.channel.send(message.language.get("COMMAND_SHARDTIMES_USER_ADDED"));
                })
                .catch(() => {
                    return super.error(message, message.language.get("COMMAND_SHARDTIMES_USER_NOT_ADDED"));
                });
        } else if (action === "remove" || action === "rem") {
            // Get the json object, remove the user if available, then resave if it changed
            if (userID !== message.author.id && userID !== message.author.username && level < 3) {
                return super.error(message, message.language.get("COMMAND_SHARDTIMES_REM_MISSING_PERMS"));
            }
            if (userID === "me") {
                userID = message.author.id;
            } else if (userID.match(/\d{17,18}/)) {
                // If they are trying to remove someone else and they don't have the right perms, stop em
                userID = userID.replace(/[\\|<|@|!]*(\d{17,18})[>]*/g,"$1");
            }
            if (shardTimes.hasOwnProperty(userID)) {
                delete shardTimes[userID];
                await Bot.database.models.shardtimes.update({times: shardTimes}, {where: {id: shardID}})
                    .then(() => {
                        return message.channel.send(message.language.get("COMMAND_SHARDTIMES_REM_SUCCESS"));
                    })
                    .catch(() => {
                        return super.error(message, message.language.get("COMMAND_SHARDTIMES_REM_FAIL"));
                    });
            } else {
                return super.error(message, message.language.get("COMMAND_SHARDTIMES_REM_MISSING"));
            }
        } else if (action === "copy") {  // ;shardtimes copy destChannel
            // Make sure the person has the correct perms to copy it (admin/ mod)
            if (level < 3) {  // Permlevel 3 is the adminRole of the server, so anyone under that shouldn"t be able to do this
                return super.error(message, message.language.get("COMMAND_EVENT_INVALID_PERMS"));
            }
            // Make sure there are times to copy from
            if (!Object.keys(shardTimes).length) {
                return super.error(message, message.language.get("COMMAND_SHARDTIMES_COPY_NO_SOURCE"));
            }
            // Check and make sure the destination channel exists/ the bot has permissions to see/ send there
            let destChan = message.guild.channels.find(c => c.name === userID);
            if (!destChan) {
                destChan = message.guild.channels.get(userID.replace(/[^0-9]/g, ""));
                if (!destChan) {
                    return super.error(message, message.language.get("COMMAND_SHARDTIMES_COPY_NO_DEST", userID));
                }
            }
            if (!destChan.permissionsFor(message.guild.me).has(["SEND_MESSAGES", "VIEW_CHANNEL"])) {
                return super.error(message, message.language.get("COMMAND_SHARDTIMES_COPY_NO_PERMS", destChan.id));
            }

            if (message.channel.id === destChan.id) {
                return super.error(message, message.language.get("COMMAND_SHARDTIMES_COPY_SAME_CHAN"));
            }

            const destShardID = `${message.guild.id}-${destChan.id}`;

            const destExists = await Bot.database.models.shardtimes.findOne({where: {id: destShardID}})
                .then(token => token != null)
                .then(isUnique => isUnique);

            if (!destExists) {
                // If there's no shard info in the destination channel
                await Bot.database.models.shardtimes.create({ id: destShardID, times: shardTimes })
                    .then(() => {
                        return message.channel.send(message.language.get("COMMAND_SHARDTIMES_COPY_SUCCESS", destChan.id));
                    })
                    .catch((err) => {
                        return super.error(message, err.message);
                    });
            } else {
                const destHasTimes = await Bot.database.models.shardtimes.findOne({where: {id: destShardID}})
                    .then(times => Object.keys(times.dataValues.times).length)
                    .then(isLen => isLen);
                if (destHasTimes) {
                    // Of if there is shard info there with listings
                    return super.error(message, message.language.get("COMMAND_SHARDTIMES_COPY_DEST_FULL"));
                } else {
                    // Or if there is shard info there, but no listings
                    await Bot.database.models.shardtimes.update({times: shardTimes}, {where: {id: destShardID}})
                        .then(() => {
                            return message.channel.send(message.language.get("COMMAND_SHARDTIMES_COPY_SUCCESS", destChan.id));
                        })
                        .catch(() => {
                            return super.error(message, message.language.get("COMMAND_SHARDTIMES_COPY_BROKE"));
                        });
                }
            }
            return;
        } else {
            // View the shard table
            const shardOut = {};
            Object.keys(shardTimes).forEach(user => {
                const diff = timeTil(shardTimes[user].timezone, timeToAdd, (shardTimes[user].zoneType ? shardTimes[user].zoneType : "zone"));
                if (shardOut.hasOwnProperty(diff)) {
                    shardOut[diff].push(user);
                } else {
                    shardOut[diff] = [user];
                }
            });

            const sortedShardTimes = Object.keys(shardOut).sort((a, b) => momentTZ(a, "HH:mm").diff(momentTZ(b, "HH:mm")));

            const fields = [];
            sortedShardTimes.forEach(time => {
                const times = [];
                shardOut[time].forEach(user => {
                    let userFlag = message.client.emojis.get(shardTimes[user].flag);
                    if (!userFlag) {
                        userFlag = shardTimes[user].flag;
                    }
                    const maxLen = 20;
                    let uName = "";
                    if (!shardTimes[user].type || shardTimes[user].type === "id") {
                        const thisUser = message.guild.members.get(user);
                        const userName = thisUser ? `${thisUser.displayName}` : user;
                        uName = "**" + (userName.length > maxLen ? userName.substring(0, maxLen) : userName) + "**";
                    } else {
                        // Type is name, don't try looking it up
                        const userName = user;
                        uName = userName.length > maxLen ? userName.substring(0, maxLen) : userName;
                    }
                    times.push({
                        flag: shardTimes[user].flag != "" ? userFlag : "",
                        name: uName
                    });
                });
                const sortedTimes = times.sort((a, b) => a.name.toLowerCase() > b.name.toLowerCase() ? 1 : 0).map(t => `${t.flag}${t.name}`);
                let joiner = " - ";
                if (message.guildSettings.shardtimeVertical) {
                    joiner = "\n";
                }
                fields.push({
                    name: time,
                    value: sortedTimes.join(joiner)
                });
            });
            return message.channel.send({
                embed: {
                    "author": {
                        "name": message.language.get("COMMAND_SHARDTIMES_SHARD_HEADER")
                    },
                    "fields": fields
                }
            });
        }

        function timeTil(zone, timeToAdd, type) {
            let targetTime;
            if (type === "zone") {
                if (momentTZ.tz(zone).unix() < momentTZ.tz(zone).startOf("day").add(timeToAdd, "h").unix()) {
                    // If it's later today
                    targetTime = momentTZ.tz(zone).startOf("day").add(timeToAdd, "h");
                } else {
                    // If it's already passed for the day
                    targetTime = momentTZ.tz(zone).startOf("day").add(1, "d").add(timeToAdd, "h");
                }
            } else if (type === "hhmm") {
                if (momentTZ.tz(zone, "HH:mm", "UTC").unix() < momentTZ().unix()) {
                    // It's already passed
                    return momentTZ.duration(momentTZ.tz(zone, "HH:mm", "UTC").add(1, "d").diff(momentTZ())).format("HH:mm", { trim: false });
                }
                return momentTZ.duration(momentTZ.tz(zone, "HH:mm", "UTC").diff(momentTZ())).format("HH:mm", { trim: false });
            } else {
                // It's utc +/- format
                if (momentTZ().utcOffset(zone).unix() < momentTZ().utcOffset(zone).startOf("day").add(timeToAdd, "h").unix()) {
                    targetTime = momentTZ().utcOffset(zone).startOf("day").add(timeToAdd, "h");
                } else {
                    targetTime = momentTZ().utcOffset(zone).startOf("day").add(1, "d").add(timeToAdd, "h");
                }
                return momentTZ.duration(targetTime.diff(momentTZ().utcOffset(zone))).format("HH:mm", { trim: false });
            }
            return momentTZ.duration(targetTime.diff(momentTZ.tz(zone))).format("HH:mm", { trim: false });
        }
    }
}

module.exports = Shardtimes;

<|endoftext|>


<commit_before>/**
 * @file TfootRow component
 * @author liangxiaojun(liangxiaojun@derbysoft.com)
 */

import React, {Component} from 'react';
import PropTypes from 'prop-types';

// Components
import Tf from '../_Tf';

// Statics
import TableFragment from '../_statics/TableFragment';
import HorizontalAlign from '../_statics/HorizontalAlign';
import SelectMode from '../_statics/SelectMode';
import SelectAllMode from '../_statics/SelectAllMode';
import SortingType from '../_statics/SortingType';
import Direction from '../_statics/Direction';

// Vendors
import classNames from 'classnames';
import Util from '../_vendors/Util';
import TC from '../_vendors/TableCalculation';
import ScrollBar from '../_vendors/ScrollBar';

class TfootRow extends Component {

    static Align = HorizontalAlign;
    static SelectMode = SelectMode;
    static SelectAllMode = SelectAllMode;
    static SortingType = SortingType;

    constructor(props, ...restArgs) {
        super(props, ...restArgs);
    }

    shouldComponentUpdate(nextProps) {
        return !nextProps.resizingColumn;
    }

    getColumnsSpan = () => {
        const {columns, tableData, onRequestColumnsSpan} = this.props;
        return onRequestColumnsSpan ?
            onRequestColumnsSpan(TableFragment.FOOT, columns, tableData)
            :
            TC.getColumnsSpan(TableFragment.FOOT, columns, tableData);
    };

    getStyle = (column, colIndex) => {
        const {columns, columnKeyField, columnsWidth, defaultColumnWidth, hasVerticalScroll} = this.props;
        return {
            ...column.footStyle,
            ...TC.getStickyColumnStyle(
                column?.fixed, colIndex, columns, columnKeyField, columnsWidth, defaultColumnWidth, hasVerticalScroll
            )
        };
    };

    handleClick = e => {
        const {tableData, disabled} = this.props;
        !disabled && this.props.onFootClick?.(tableData, e);
    };

    render() {

        const {
                data, tableData, footData, disabled, ignoreColumnSpan, rowIndex,
                scrollEl, hasVerticalScroll, hasFixedRightColumn,
                onCellClick
            } = this.props,
            columnsSpan = this.getColumnsSpan(),
            verticalScrollBarSize = ScrollBar.getSize(Direction.VERTICAL);

        return (

                {
                    columnsSpan?.map(({column, span}, colIndex) =>
                        <Tf key={colIndex}
                            className={classNames(column.footClassName, {
                                'fixed-left': column.fixed === HorizontalAlign.LEFT,
                                'last-fixed-left': column.fixed === HorizontalAlign.LEFT
                                    && columnsSpan?.[colIndex + 1]?.column?.fixed !== HorizontalAlign.LEFT,
                                'fixed-right': column.fixed === HorizontalAlign.RIGHT,
                                'first-fixed-right': column.fixed === HorizontalAlign.RIGHT
                                    && columnsSpan?.[colIndex - 1]?.column?.fixed !== HorizontalAlign.RIGHT
                            })}
                            style={this.getStyle(column, colIndex)}
                            rowIndex={rowIndex}
                            colIndex={colIndex}
                            rowData={data}
                            tableData={tableData}
                            footData={footData}
                            title={column.footTitle}
                            renderer={column.footRenderer}
                            bodyRenderer={column.bodyRenderer}
                            align={column.footAlign || column.align}
                            span={ignoreColumnSpan ? null : span}
                            noWrap={TC.handleNoWrap(column.footNoWrap, column.noWrap, {
                                data: tableData,
                                rowIndex: 0,
                                colIndex: colIndex,
                                tableData
                            })}
                            disabled={disabled}
                            scrollEl={scrollEl}
                            onCellClick={onCellClick}/>
                    )
                }

                {
                    hasVerticalScroll && columnsSpan && verticalScrollBarSize > 0 ?

                        :
                        null
                }

        );

    }
}

TfootRow.propTypes = {

    /**
     * Children passed into table header.
     */
    columns: PropTypes.arrayOf(PropTypes.shape({

        /**
         * unique keyof column.
         */
        key: PropTypes.string,

        /**
         * fixed position of column ( 'left' / 'right' ).
         */
        fixed: PropTypes.oneOf(Util.enumerateValue(HorizontalAlign)),

        /**
         * width of column.
         */
        width: PropTypes.oneOfType([PropTypes.number, PropTypes.string, PropTypes.func]),

        /**
         * minimum width of column.
         */
        minWidth: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),

        /**
         * align of current column.
         */
        align: PropTypes.oneOf(Util.enumerateValue(HorizontalAlign)),

        /**
         * no wrap of current column.
         */
        noWrap: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),

        /**
         * The class name of header.
         */
        headClassName: PropTypes.string,

        /**
         * Override the styles of header.
         */
        headStyle: PropTypes.object,

        /**
         * align of table header cell
         */
        headAlign: PropTypes.oneOf(Util.enumerateValue(HorizontalAlign)),

        /**
         * The render content in table head.
         *  (1) callback:
         *      function (tableData, colIndex) {
         *          return colIndex;
         *      }
         *
         *  (2) others:
         *      render whatever you pass
         */
        headRenderer: PropTypes.any,

        /**
         * The title in table head.
         *  (1) callback:
         *      function (tableData, colIndex) {
         *          return colIndex;
         *      }
         *
         *  (2) others:
         *      render whatever you pass
         */
        headTitle: PropTypes.any,

        /**
         * column span of table header.
         *  (1) function callback:
         *      function (tableData, colIndex) {
         *          return null;
         *      }
         *
         *  (2) number:
         *      render whatever you pass
         */
        headSpan: PropTypes.oneOfType([PropTypes.number, PropTypes.func]),

        /**
         * no wrap of table header.
         */
        headNoWrap: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),

        /**
         * The class name of td.
         */
        bodyClassName: PropTypes.string,

        /**
         * Override the styles of td.
         */
        bodyStyle: PropTypes.object,

        /**
         * align of table body cell.
         */
        bodyAlign: PropTypes.oneOf(Util.enumerateValue(HorizontalAlign)),

        /**
         * The render content in table body.
         *  (1) callback:
         *      function (rowData, rowIndex, colIndex, parentData, tableData, collapsed, depth, path) {
         *          return rowData.id;
         *      }
         *
         *  (2) others:
         *      render whatever you pass
         */
        bodyRenderer: PropTypes.any,

        /**
         * The title in table body.
         *  (1) callback:
         *      function (rowData, rowIndex, colIndex, parentData, tableData, collapsed, depth, path) {
         *          return rowData.id;
         *      }
         *
         *  (2) others:
         *      render whatever you pass
         */
        bodyTitle: PropTypes.any,

        /**
         * column span of table body.
         *  (1) function callback:
         *      function (rowData, colIndex, rowIndex) {
         *          return null;
         *      }
         *
         *  (2) number:
         *      render whatever you pass
         */
        bodySpan: PropTypes.oneOfType([PropTypes.number, PropTypes.func]),

        /**
         * no wrap of table body.
         */
        bodyNoWrap: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),

        /**
         * The class name of footer.
         */
        footClassName: PropTypes.string,

        /**
         * Override the styles of footer.
         */
        footStyle: PropTypes.object,

        /**
         * align of table footer cell
         */
        footAlign: PropTypes.oneOf(Util.enumerateValue(HorizontalAlign)),

        /**
         * The render content in table foot.
         *  (1) callback:
         *      function (tableData, colIndex) {
         *          return colIndex;
         *      }
         *
         *  (2) others:
         *      render whatever you pass
         */
        footRenderer: PropTypes.any,

        /**
         * The title in table foot.
         *  (1) callback:
         *      function (tableData, colIndex) {
         *          return colIndex;
         *      }
         *
         *  (2) others:
         *      render whatever you pass
         */
        footTitle: PropTypes.any,

        /**
         * column span of table foot.
         *  (1) function callback:
         *      function (tableData, colIndex) {
         *          return null;
         *      }
         *
         *  (2) number:
         *      render whatever you pass
         */
        footSpan: PropTypes.oneOfType([PropTypes.number, PropTypes.func]),

        /**
         * no wrap of table foot.
         */
        footNoWrap: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),

        /**
         * If true, this column can be sorted.
         */
        sortable: PropTypes.bool,

        /**
         * Sorting property.
         */
        sortingProp: PropTypes.string,

        defaultSortingType: PropTypes.oneOf(Util.enumerateValue(SortingType)),

        /**
         * whether the column can be resized
         */
        resizable: PropTypes.bool

    })).isRequired,
    columnKeyField: PropTypes.string,
    columnsWidth: PropTypes.object,
    data: PropTypes.object,
    tableData: PropTypes.array,
    footData: PropTypes.array,
    disabled: PropTypes.bool,
    ignoreColumnSpan: PropTypes.bool,
    rowIndex: PropTypes.number,

    /**
     * scroll
     */
    scrollEl: PropTypes.object,
    hasVerticalScroll: PropTypes.bool,

    /**
     * fixed
     */
    hasFixedRightColumn: PropTypes.bool,

    /**
     * column resizable
     */
    defaultColumnWidth: PropTypes.number,
    resizingColumn: PropTypes.object,

    onFootClick: PropTypes.func,
    onCellClick: PropTypes.func,
    onRequestColumnsSpan: PropTypes.func

};

TfootRow.defaultProps = {

    columnKeyField: 'key',
    disabled: false,
    ignoreColumnSpan: false,

    /**
     * scroll
     */
    hasVerticalScroll: false,

    /**
     * fixed
     */
    hasFixedRightColumn: false,

    /**
     * column resizable
     */
    defaultColumnWidth: 100

};

export default TfootRow;
<commit_msg>update TfootRow<commit_after>/**
 * @file TfootRow component
 * @author liangxiaojun(liangxiaojun@derbysoft.com)
 */

import React, {Component} from 'react';
import PropTypes from 'prop-types';

// Components
import Tf from '../_Tf';

// Statics
import TableFragment from '../_statics/TableFragment';
import HorizontalAlign from '../_statics/HorizontalAlign';
import SelectMode from '../_statics/SelectMode';
import SelectAllMode from '../_statics/SelectAllMode';
import SortingType from '../_statics/SortingType';
import Direction from '../_statics/Direction';

// Vendors
import classNames from 'classnames';
import Util from '../_vendors/Util';
import TC from '../_vendors/TableCalculation';
import ScrollBar from '../_vendors/ScrollBar';

class TfootRow extends Component {

    static Align = HorizontalAlign;
    static SelectMode = SelectMode;
    static SelectAllMode = SelectAllMode;
    static SortingType = SortingType;

    constructor(props, ...restArgs) {
        super(props, ...restArgs);
    }

    shouldComponentUpdate(nextProps) {
        return !nextProps.resizingColumn;
    }

    getColumnsSpan = () => {
        const {columns, tableData, onRequestColumnsSpan} = this.props;
        return onRequestColumnsSpan ?
            onRequestColumnsSpan(TableFragment.FOOT, columns, tableData)
            :
            TC.getColumnsSpan(TableFragment.FOOT, columns, tableData);
    };

    getStyle = (column, originColumnIndex) => {
        const {columns, columnKeyField, columnsWidth, defaultColumnWidth, hasVerticalScroll} = this.props;
        return {
            ...column.footStyle,
            ...TC.getStickyColumnStyle(
                column?.fixed, originColumnIndex, columns,
                columnKeyField, columnsWidth, defaultColumnWidth, hasVerticalScroll
            )
        };
    };

    handleClick = e => {
        const {tableData, disabled} = this.props;
        !disabled && this.props.onFootClick?.(tableData, e);
    };

    render() {

        const {
                data, tableData, footData, disabled, ignoreColumnSpan, rowIndex,
                scrollEl, hasVerticalScroll, hasFixedRightColumn,
                onCellClick
            } = this.props,
            columnsSpan = this.getColumnsSpan(),
            verticalScrollBarSize = ScrollBar.getSize(Direction.VERTICAL);

        return (

                {
                    columnsSpan?.map(({column, span, originColumnIndex}, colIndex) =>
                        <Tf key={colIndex}
                            className={classNames(column.footClassName, {
                                'fixed-left': column.fixed === HorizontalAlign.LEFT,
                                'last-fixed-left': column.fixed === HorizontalAlign.LEFT
                                    && columnsSpan?.[colIndex + 1]?.column?.fixed !== HorizontalAlign.LEFT,
                                'fixed-right': column.fixed === HorizontalAlign.RIGHT,
                                'first-fixed-right': column.fixed === HorizontalAlign.RIGHT
                                    && columnsSpan?.[colIndex - 1]?.column?.fixed !== HorizontalAlign.RIGHT
                            })}
                            style={this.getStyle(column, originColumnIndex)}
                            rowIndex={rowIndex}
                            colIndex={colIndex}
                            rowData={data}
                            tableData={tableData}
                            footData={footData}
                            title={column.footTitle}
                            renderer={column.footRenderer}
                            bodyRenderer={column.bodyRenderer}
                            align={column.footAlign || column.align}
                            span={ignoreColumnSpan ? null : span}
                            noWrap={TC.handleNoWrap(column.footNoWrap, column.noWrap, {
                                data: tableData,
                                rowIndex: 0,
                                colIndex: colIndex,
                                tableData
                            })}
                            disabled={disabled}
                            scrollEl={scrollEl}
                            onCellClick={onCellClick}/>
                    )
                }

                {
                    hasVerticalScroll && columnsSpan && verticalScrollBarSize > 0 ?

                        :
                        null
                }

        );

    }
}

TfootRow.propTypes = {

    /**
     * Children passed into table header.
     */
    columns: PropTypes.arrayOf(PropTypes.shape({

        /**
         * unique keyof column.
         */
        key: PropTypes.string,

        /**
         * fixed position of column ( 'left' / 'right' ).
         */
        fixed: PropTypes.oneOf(Util.enumerateValue(HorizontalAlign)),

        /**
         * width of column.
         */
        width: PropTypes.oneOfType([PropTypes.number, PropTypes.string, PropTypes.func]),

        /**
         * minimum width of column.
         */
        minWidth: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),

        /**
         * align of current column.
         */
        align: PropTypes.oneOf(Util.enumerateValue(HorizontalAlign)),

        /**
         * no wrap of current column.
         */
        noWrap: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),

        /**
         * The class name of header.
         */
        headClassName: PropTypes.string,

        /**
         * Override the styles of header.
         */
        headStyle: PropTypes.object,

        /**
         * align of table header cell
         */
        headAlign: PropTypes.oneOf(Util.enumerateValue(HorizontalAlign)),

        /**
         * The render content in table head.
         *  (1) callback:
         *      function (tableData, colIndex) {
         *          return colIndex;
         *      }
         *
         *  (2) others:
         *      render whatever you pass
         */
        headRenderer: PropTypes.any,

        /**
         * The title in table head.
         *  (1) callback:
         *      function (tableData, colIndex) {
         *          return colIndex;
         *      }
         *
         *  (2) others:
         *      render whatever you pass
         */
        headTitle: PropTypes.any,

        /**
         * column span of table header.
         *  (1) function callback:
         *      function (tableData, colIndex) {
         *          return null;
         *      }
         *
         *  (2) number:
         *      render whatever you pass
         */
        headSpan: PropTypes.oneOfType([PropTypes.number, PropTypes.func]),

        /**
         * no wrap of table header.
         */
        headNoWrap: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),

        /**
         * The class name of td.
         */
        bodyClassName: PropTypes.string,

        /**
         * Override the styles of td.
         */
        bodyStyle: PropTypes.object,

        /**
         * align of table body cell.
         */
        bodyAlign: PropTypes.oneOf(Util.enumerateValue(HorizontalAlign)),

        /**
         * The render content in table body.
         *  (1) callback:
         *      function (rowData, rowIndex, colIndex, parentData, tableData, collapsed, depth, path) {
         *          return rowData.id;
         *      }
         *
         *  (2) others:
         *      render whatever you pass
         */
        bodyRenderer: PropTypes.any,

        /**
         * The title in table body.
         *  (1) callback:
         *      function (rowData, rowIndex, colIndex, parentData, tableData, collapsed, depth, path) {
         *          return rowData.id;
         *      }
         *
         *  (2) others:
         *      render whatever you pass
         */
        bodyTitle: PropTypes.any,

        /**
         * column span of table body.
         *  (1) function callback:
         *      function (rowData, colIndex, rowIndex) {
         *          return null;
         *      }
         *
         *  (2) number:
         *      render whatever you pass
         */
        bodySpan: PropTypes.oneOfType([PropTypes.number, PropTypes.func]),

        /**
         * no wrap of table body.
         */
        bodyNoWrap: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),

        /**
         * The class name of footer.
         */
        footClassName: PropTypes.string,

        /**
         * Override the styles of footer.
         */
        footStyle: PropTypes.object,

        /**
         * align of table footer cell
         */
        footAlign: PropTypes.oneOf(Util.enumerateValue(HorizontalAlign)),

        /**
         * The render content in table foot.
         *  (1) callback:
         *      function (tableData, colIndex) {
         *          return colIndex;
         *      }
         *
         *  (2) others:
         *      render whatever you pass
         */
        footRenderer: PropTypes.any,

        /**
         * The title in table foot.
         *  (1) callback:
         *      function (tableData, colIndex) {
         *          return colIndex;
         *      }
         *
         *  (2) others:
         *      render whatever you pass
         */
        footTitle: PropTypes.any,

        /**
         * column span of table foot.
         *  (1) function callback:
         *      function (tableData, colIndex) {
         *          return null;
         *      }
         *
         *  (2) number:
         *      render whatever you pass
         */
        footSpan: PropTypes.oneOfType([PropTypes.number, PropTypes.func]),

        /**
         * no wrap of table foot.
         */
        footNoWrap: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),

        /**
         * If true, this column can be sorted.
         */
        sortable: PropTypes.bool,

        /**
         * Sorting property.
         */
        sortingProp: PropTypes.string,

        defaultSortingType: PropTypes.oneOf(Util.enumerateValue(SortingType)),

        /**
         * whether the column can be resized
         */
        resizable: PropTypes.bool

    })).isRequired,
    columnKeyField: PropTypes.string,
    columnsWidth: PropTypes.object,
    data: PropTypes.object,
    tableData: PropTypes.array,
    footData: PropTypes.array,
    disabled: PropTypes.bool,
    ignoreColumnSpan: PropTypes.bool,
    rowIndex: PropTypes.number,

    /**
     * scroll
     */
    scrollEl: PropTypes.object,
    hasVerticalScroll: PropTypes.bool,

    /**
     * fixed
     */
    hasFixedRightColumn: PropTypes.bool,

    /**
     * column resizable
     */
    defaultColumnWidth: PropTypes.number,
    resizingColumn: PropTypes.object,

    onFootClick: PropTypes.func,
    onCellClick: PropTypes.func,
    onRequestColumnsSpan: PropTypes.func

};

TfootRow.defaultProps = {

    columnKeyField: 'key',
    disabled: false,
    ignoreColumnSpan: false,

    /**
     * scroll
     */
    hasVerticalScroll: false,

    /**
     * fixed
     */
    hasFixedRightColumn: false,

    /**
     * column resizable
     */
    defaultColumnWidth: 100

};

export default TfootRow;
<|endoftext|>


<commit_before>const {Plugin, PluginKey} = require("prosemirror-state")
const RopeSequence = require("rope-sequence")

// : (Transform, [Step], [Step], [Step]) → number
// Undo a given set of steps, apply a set of other steps, and then
// redo them.
function rebaseSteps(transform, steps, inverted, inside) {
  for (let i = inverted.length - 1; i >= 0; i--) transform.step(inverted[i])
  for (let i = 0; i < inside.length; i++) transform.step(inside[i])
  for (let i = 0, mapFrom = inverted.length; i < steps.length; i++) {
    let mapped = steps[i].map(transform.mapping.slice(mapFrom))
    mapFrom--
    if (mapped && !transform.maybeStep(mapped).failed)
      transform.mapping.setMirror(mapFrom, transform.steps.length - 1)
  }
  return inverted.length + inside.length
}
exports.rebaseSteps = rebaseSteps

// This state field accumulates changes that have to be sent to the
// central authority in the collaborating group and makes it possible
// to integrate changes made by peers into our local document. It is
// defined by the plugin, and will be available as the `collab` field
// in the resulting editor state.
class CollabState {
  constructor(version, unconfirmed) {
    // : number
    // The version number of the last update received from the central
    // authority. Starts at 0 or the value of the `version` property
    // in the option object, for the editor's value when the option
    // was enabled.
    this.version = version

    // : RopeSequence<{step: Step, inverted: Step}>
    // The local steps that havent been successfully sent to the
    // server yet.
    this.unconfirmed = unconfirmed
  }
}

function unconfirmedFrom(transform, start = 0) {
  let add = []
  for (let i = start; i < transform.steps.length; i++)
    add.push({step: transform.steps[i],
              inverted: transform.steps[i].invert(transform.docs[i])})
  return add
}

// Pushes a set of steps (received from the central authority) into
// the editor state (which should have the collab plugin enabled).
// Will recognize its own changes, and confirm unconfirmed steps as
// appropriate. Remaining unconfirmed steps will be rebased over
// remote steps.
function makeReceiveAction(state, steps, clientIDs, ourID) {
  let collabState = collabKey.getState(state)
  let version = collabState.version + steps.length

  // Find out which prefix of the steps originated with us
  let ours = 0
  while (ours < clientIDs.length && clientIDs[ours] == ourID) ++ours
  let unconfirmed = collabState.unconfirmed.slice(ours)
  steps = ours ? steps.slice(ours) : steps

  // If all steps originated with us, we're done.
  if (!steps.length)
    return {type: "collabConfirm", collabState: new CollabState(version, unconfirmed)}

  let nUnconfirmed = unconfirmed.length
  let transform = state.tr
  if (nUnconfirmed) {
    rebaseSteps(transform, unconfirmed.map(s => s.step), unconfirmed.map(s => s.inverted), steps)
  } else {
    for (let i = 0; i < steps.length; i++) transform.step(steps[i])
  }

  unconfirmed = RopeSequence.from(unconfirmedFrom(transform, nUnconfirmed + steps.length))
  let newCollabState = new CollabState(version, unconfirmed)
  return transform.action({
    rebased: nUnconfirmed,
    addToHistory: false,
    collabState: newCollabState,
    interaction: false
  })
}

const collabKey = new PluginKey("collab")

// :: (?Object) → Plugin
//
// Creates a plugin that enables the collaborative editing framework
// for the editor.
//
//   config::- An optional set of options
//
//     version:: ?number
//     The starting version number of the collaborative editing.
//     Defaults to 0.
//
//     clientID:: ?number
//     This client's ID, used to distinguish its changes from those of
//     other clients. Defaults to a random 32-bit number.
function collab(config = {}) {
  config = {version: config.version || 0,
            clientID: config.clientID == null ? Math.floor(Math.random() * 0xFFFFFFFF) : config.clientID}

  return new Plugin({
    key: collabKey,

    state: {
      init: () => new CollabState(config.version, RopeSequence.empty),
      applyAction(action, collab) {
        if (action.type == "transform")
          return action.collabState ||
            new CollabState(collab.version, collab.unconfirmed.append(unconfirmedFrom(action.transform)))
        if (action.type == "collabConfirm")
          return action.collabState
        return collab
      }
    },

    config
  })
}
exports.collab = collab

// :: (state: EditorState, steps: [Step], clientIDs: [number]) → Action
// Create an action that represents a set of new steps received from
// the authority. Applying this action moves the state forward to
// adjust to the authority's view of the document.
function receiveAction(state, steps, clientIDs) {
  return makeReceiveAction(state, steps, clientIDs, collabKey.get(state).options.config.clientID)
}
exports.receiveAction = receiveAction

// :: (state: EditorState) → ?{version: number, steps: [Step], clientID: number}
// Provides the data describing the editor's unconfirmed steps, which
// you'd send to the central authority. Returns null when there is
// nothing to send.
function sendableSteps(state) {
  let collabState = collabKey.getState(state)
  if (collabState.unconfirmed.length == 0) return null
  return {
    version: collabState.version,
    steps: collabState.unconfirmed.map(s => s.step),
    clientID: collabKey.get(state).options.config.clientID
  }
}
exports.sendableSteps = sendableSteps

// :: (EditorState) → number
// Get the version up to which the collab plugin has synced with the
// central authority.
function getVersion(state) {
  return collabKey.getState(state).version
}
exports.getVersion = getVersion
<commit_msg>Mark transform actions as sealed<commit_after>const {Plugin, PluginKey} = require("prosemirror-state")
const RopeSequence = require("rope-sequence")

// : (Transform, [Step], [Step], [Step]) → number
// Undo a given set of steps, apply a set of other steps, and then
// redo them.
function rebaseSteps(transform, steps, inverted, inside) {
  for (let i = inverted.length - 1; i >= 0; i--) transform.step(inverted[i])
  for (let i = 0; i < inside.length; i++) transform.step(inside[i])
  for (let i = 0, mapFrom = inverted.length; i < steps.length; i++) {
    let mapped = steps[i].map(transform.mapping.slice(mapFrom))
    mapFrom--
    if (mapped && !transform.maybeStep(mapped).failed)
      transform.mapping.setMirror(mapFrom, transform.steps.length - 1)
  }
  return inverted.length + inside.length
}
exports.rebaseSteps = rebaseSteps

// This state field accumulates changes that have to be sent to the
// central authority in the collaborating group and makes it possible
// to integrate changes made by peers into our local document. It is
// defined by the plugin, and will be available as the `collab` field
// in the resulting editor state.
class CollabState {
  constructor(version, unconfirmed) {
    // : number
    // The version number of the last update received from the central
    // authority. Starts at 0 or the value of the `version` property
    // in the option object, for the editor's value when the option
    // was enabled.
    this.version = version

    // : RopeSequence<{step: Step, inverted: Step}>
    // The local steps that havent been successfully sent to the
    // server yet.
    this.unconfirmed = unconfirmed
  }
}

function unconfirmedFrom(transform, start = 0) {
  let add = []
  for (let i = start; i < transform.steps.length; i++)
    add.push({step: transform.steps[i],
              inverted: transform.steps[i].invert(transform.docs[i])})
  return add
}

// Pushes a set of steps (received from the central authority) into
// the editor state (which should have the collab plugin enabled).
// Will recognize its own changes, and confirm unconfirmed steps as
// appropriate. Remaining unconfirmed steps will be rebased over
// remote steps.
function makeReceiveAction(state, steps, clientIDs, ourID) {
  let collabState = collabKey.getState(state)
  let version = collabState.version + steps.length

  // Find out which prefix of the steps originated with us
  let ours = 0
  while (ours < clientIDs.length && clientIDs[ours] == ourID) ++ours
  let unconfirmed = collabState.unconfirmed.slice(ours)
  steps = ours ? steps.slice(ours) : steps

  // If all steps originated with us, we're done.
  if (!steps.length)
    return {type: "collabConfirm", collabState: new CollabState(version, unconfirmed)}

  let nUnconfirmed = unconfirmed.length
  let transform = state.tr
  if (nUnconfirmed) {
    rebaseSteps(transform, unconfirmed.map(s => s.step), unconfirmed.map(s => s.inverted), steps)
  } else {
    for (let i = 0; i < steps.length; i++) transform.step(steps[i])
  }

  unconfirmed = RopeSequence.from(unconfirmedFrom(transform, nUnconfirmed + steps.length))
  let newCollabState = new CollabState(version, unconfirmed)
  return transform.action({
    rebased: nUnconfirmed,
    addToHistory: false,
    collabState: newCollabState,
    sealed: true
  })
}

const collabKey = new PluginKey("collab")

// :: (?Object) → Plugin
//
// Creates a plugin that enables the collaborative editing framework
// for the editor.
//
//   config::- An optional set of options
//
//     version:: ?number
//     The starting version number of the collaborative editing.
//     Defaults to 0.
//
//     clientID:: ?number
//     This client's ID, used to distinguish its changes from those of
//     other clients. Defaults to a random 32-bit number.
function collab(config = {}) {
  config = {version: config.version || 0,
            clientID: config.clientID == null ? Math.floor(Math.random() * 0xFFFFFFFF) : config.clientID}

  return new Plugin({
    key: collabKey,

    state: {
      init: () => new CollabState(config.version, RopeSequence.empty),
      applyAction(action, collab) {
        if (action.type == "transform")
          return action.collabState ||
            new CollabState(collab.version, collab.unconfirmed.append(unconfirmedFrom(action.transform)))
        if (action.type == "collabConfirm")
          return action.collabState
        return collab
      }
    },

    config
  })
}
exports.collab = collab

// :: (state: EditorState, steps: [Step], clientIDs: [number]) → Action
// Create an action that represents a set of new steps received from
// the authority. Applying this action moves the state forward to
// adjust to the authority's view of the document.
function receiveAction(state, steps, clientIDs) {
  return makeReceiveAction(state, steps, clientIDs, collabKey.get(state).options.config.clientID)
}
exports.receiveAction = receiveAction

// :: (state: EditorState) → ?{version: number, steps: [Step], clientID: number}
// Provides the data describing the editor's unconfirmed steps, which
// you'd send to the central authority. Returns null when there is
// nothing to send.
function sendableSteps(state) {
  let collabState = collabKey.getState(state)
  if (collabState.unconfirmed.length == 0) return null
  return {
    version: collabState.version,
    steps: collabState.unconfirmed.map(s => s.step),
    clientID: collabKey.get(state).options.config.clientID
  }
}
exports.sendableSteps = sendableSteps

// :: (EditorState) → number
// Get the version up to which the collab plugin has synced with the
// central authority.
function getVersion(state) {
  return collabKey.getState(state).version
}
exports.getVersion = getVersion
<|endoftext|>


<commit_before>/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2015 Mickael Jeanroy
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

/* jshint eqnull:true */

/* global _ */
/* global $parse */
/* global $sanitize */
/* global $renderers */
/* global $comparators */
/* global CSS_SORTABLE */
/* global CSS_SORTABLE_DESC */
/* global CSS_SORTABLE_ASC */

var __searchIn = function(value, dictionary, def) {
  // If value is a string, search in given dictionary
  if (_.isString(value)) {
    value = dictionary[value];
  }

  // If it is not a function then use default value in dictionary
  if (!_.isFunction(value)) {
    value = dictionary[def];
  }

  return value;
};

var Column = function(column) {
  var isUndefined = _.isUndefined;
  var escape = column.escape;
  var sortable = column.sortable;

  this.id = column.id;
  this.title = column.title || '';
  this.field = column.field || this.id;
  this.css = column.css || this.id;
  this.escape = isUndefined(escape) ? true : !!escape;

  this.sortable = isUndefined(sortable) ? true : !!sortable;
  this.order = column.order || '';

  // Sanitize input at construction
  if (escape) {
    this.title = $sanitize(this.title);
  }

  // Renderer can be defined as a custom function
  // Or it could be defined a string which is a shortcut to a pre-built renderer
  // If it is not a function, switch to default renderer
  // TODO Is it really a good idea ? Should we allow more flexibility ?
  var defaultRenderer = 'identity';
  var columnRenderer = column.renderer || [defaultRenderer];
  var renderers = _.isArray(column.renderer) ? column.renderer : [column.renderer];
  this.renderer = _.map(renderers, function(r) {
    return __searchIn(r, $renderers, defaultRenderer);
  });

  // Comparator can be defined as a custom function
  // Or it could be defined a string which is a shortcut to a pre-built comparator
  // If it is not a function, switch to default comparator
  this.comparator = __searchIn(column.comparator, $comparators, '$auto');

  // Parse that will be used to extract data value from plain old javascript object
  this.$parser = $parse(this.field);
};

Column.prototype = {
  // Get css class to append to each cell
  cssClasses: function() {
    var classes = [this.css];

    if (this.sortable) {
      // Add css to display that column is sortable
      classes.push(CSS_SORTABLE);

      // Add css to display current sort
      if (this.order) {
        classes.push(this.order === '-' ? CSS_SORTABLE_DESC : CSS_SORTABLE_ASC);
      }
    }

    return classes;
  },

  // Render object using column settings
  render: function(object) {
    // Extract value
    var field = this.field;
    var value = object == null ? '' : this.$parser(object);

    // Give extracted value as the first parameter
    // Give object as the second parameter to allow more flexibility
    // Give field to display as the third parameter to allow more flexibility
    // Use '$renderers' as this context, this way renderers can be easy compose
    var rendererValue = _.reduce(this.renderer, function(val, r) {
      return r.call($renderers, val, object, field);
    }, value);

    // If value is null or undefined, fallback to an empty string
    if (rendererValue == null) {
      return '';
    }

    return this.escape ? $sanitize(rendererValue) : rendererValue;
  }
};
<commit_msg>fix: use real column renderer<commit_after>/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2015 Mickael Jeanroy
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

/* jshint eqnull:true */

/* global _ */
/* global $parse */
/* global $sanitize */
/* global $renderers */
/* global $comparators */
/* global CSS_SORTABLE */
/* global CSS_SORTABLE_DESC */
/* global CSS_SORTABLE_ASC */

var __searchIn = function(value, dictionary, def) {
  // If value is a string, search in given dictionary
  if (_.isString(value)) {
    value = dictionary[value];
  }

  // If it is not a function then use default value in dictionary
  if (!_.isFunction(value)) {
    value = dictionary[def];
  }

  return value;
};

var Column = function(column) {
  var isUndefined = _.isUndefined;
  var escape = column.escape;
  var sortable = column.sortable;

  this.id = column.id;
  this.title = column.title || '';
  this.field = column.field || this.id;
  this.css = column.css || this.id;
  this.escape = isUndefined(escape) ? true : !!escape;

  this.sortable = isUndefined(sortable) ? true : !!sortable;
  this.order = column.order || '';

  // Sanitize input at construction
  if (escape) {
    this.title = $sanitize(this.title);
  }

  // Renderer can be defined as a custom function
  // Or it could be defined a string which is a shortcut to a pre-built renderer
  // If it is not a function, switch to default renderer
  // TODO Is it really a good idea ? Should we allow more flexibility ?
  var defaultRenderer = 'identity';
  var columnRenderer = column.renderer || defaultRenderer;
  var renderers = _.isArray(columnRenderer) ? columnRenderer : [columnRenderer];
  this.renderer = _.map(renderers, function(r) {
    return __searchIn(r, $renderers, defaultRenderer);
  });

  // Comparator can be defined as a custom function
  // Or it could be defined a string which is a shortcut to a pre-built comparator
  // If it is not a function, switch to default comparator
  this.comparator = __searchIn(column.comparator, $comparators, '$auto');

  // Parse that will be used to extract data value from plain old javascript object
  this.$parser = $parse(this.field);
};

Column.prototype = {
  // Get css class to append to each cell
  cssClasses: function() {
    var classes = [this.css];

    if (this.sortable) {
      // Add css to display that column is sortable
      classes.push(CSS_SORTABLE);

      // Add css to display current sort
      if (this.order) {
        classes.push(this.order === '-' ? CSS_SORTABLE_DESC : CSS_SORTABLE_ASC);
      }
    }

    return classes;
  },

  // Render object using column settings
  render: function(object) {
    // Extract value
    var field = this.field;
    var value = object == null ? '' : this.$parser(object);

    // Give extracted value as the first parameter
    // Give object as the second parameter to allow more flexibility
    // Give field to display as the third parameter to allow more flexibility
    // Use '$renderers' as this context, this way renderers can be easy compose
    var rendererValue = _.reduce(this.renderer, function(val, r) {
      return r.call($renderers, val, object, field);
    }, value);

    // If value is null or undefined, fallback to an empty string
    if (rendererValue == null) {
      return '';
    }

    return this.escape ? $sanitize(rendererValue) : rendererValue;
  }
};
<|endoftext|>


<commit_before>import { getCandidateCatalogs, computeFreshnessScore, isObsolete, computeOpenScore, computeDownloadableScore, computeCatalogScore } from '../catalogs'
import { get } from 'lodash'

describe('catalogs', function () {
  const currentDate = new Date('2016-07-15')

  describe('getCandidateCatalogs(catalogs, sourceCatalogs)', function () {
    describe('catalog download or openness data is not defined', () => {
      it('should not return catalog', () => {
        const sourceCatalogs = ['1', '2', '3']
        const catalog = { id: '4', metrics: null }
        const catalogs = [catalog]

        expect(getCandidateCatalogs(catalogs, sourceCatalogs)).to.eql([])
      })
    })

    describe('catalog has no open or download data', () => {
      it('should not return catalog', () => {
        const sourceCatalogs = ['1', '2', '3']
        const catalog = { id: '4', metrics: { datasets: { partitions: { openness: { yes: 0 }, download: { yes: 0 } } } } }
        const catalogs = [catalog]

        expect(getCandidateCatalogs(catalogs, sourceCatalogs)).to.eql([])
      })
    })

    describe('catalog has at least one open and download data', () => {
      it('should return catalog', () => {
        const sourceCatalogs = ['1', '2', '3']
        const catalog = { id: '4', metrics: { datasets: { partitions: { openness: { yes: 42 }, download: { yes: 1 } } } } }
        const catalogs = [catalog]

        expect(getCandidateCatalogs(catalogs, sourceCatalogs)).to.eql([catalog])
      })
    })

    describe('catalog is already in catalogSources', () => {
      it('should not return catalog', () => {
        const sourceCatalogs = ['1', '2', '3']
        const catalog = { id: '1', metrics: { datasets: { partitions: { openness: { yes: 42 }, download: { yes: 1 } } } } }
        const catalogs = [catalog]

        expect(getCandidateCatalogs(catalogs, sourceCatalogs)).to.eql([])
      })
    })
  })

  describe('computeFreshnessScore(moment)', function () {
    describe('moment is not defined', function () {
      it('should return 0', function () {
        expect(computeFreshnessScore(null, currentDate)).to.equal(1)
      })
    })
    describe('moment is more recent than 1 month', function () {
      it('should return 100', function () {
        expect(computeFreshnessScore(new Date('2016-07-01'), currentDate)).to.equal(100)
      })
    })
    describe('moment is more recent than 6 months', function () {
      it('should return 10', function () {
        expect(computeFreshnessScore(new Date('2016-05-01'), currentDate)).to.equal(10)
      })
    })
    describe('moment is older than 6 months', function () {
      it('should return 0', function () {
        expect(computeFreshnessScore(new Date('2011-07-01'), currentDate)).to.equal(1)
      })
    })
  })

  describe('isObsolete(catalog)', () => {
    describe('catalog has no mostRecentRevisionDate metric', () => {
      const catalog = {}
      it('should return undefined', function () {
        return expect(isObsolete(catalog, currentDate)).to.be.undefined
      })
    })
    describe('mostRecentRevisionDate is more recent than 6 months', () => {
      const catalog = { metrics: { mostRecentRevisionDate: new Date('2016-06-01') } }
      it('should return false', function () {
        return expect(isObsolete(catalog, currentDate)).to.be.false
      })
    })
    describe('mostRecentRevisionDate is older than 6 months', () => {
      const catalog = { metrics: { mostRecentRevisionDate: new Date('2011-06-01') } }
      it('should return true', function () {
        return expect(isObsolete(catalog, currentDate)).to.be.true
      })
    })
  })

  describe('computeOpenScore(percentOpen)', () => {
    describe('percentOpen >= 100', () => {
      it('should return 100', () => {
        expect(computeOpenScore(500)).to.equal(100)
        expect(computeOpenScore(100)).to.equal(100)
      })
    })
    describe('percentOpen <= 1', () => {
      it('should return 1', () => {
        expect(computeOpenScore(-10)).to.equal(1)
        expect(computeOpenScore(0)).to.equal(1)
        expect(computeOpenScore(1)).to.equal(1)
      })
    })
    describe('1 < percentOpen < 100', () => {
      it('should return the same value', () => {
        expect(computeOpenScore(47)).to.equal(47)
      })
    })
  })

  describe('computeDownloadableScore(percentDownloadable)', () => {
    describe('percentDownloadable >= 100', () => {
      it('should return 100 * 100', () => {
        expect(computeDownloadableScore(500)).to.equal(100 * 100)
        expect(computeDownloadableScore(100)).to.equal(100 * 100)
      })
    })
    describe('percentDownloadable <= 1', () => {
      it('should return 1', () => {
        expect(computeDownloadableScore(-10)).to.equal(1)
        expect(computeDownloadableScore(0)).to.equal(1)
        expect(computeDownloadableScore(1)).to.equal(1)
      })
    })
    describe('1 < percentDownloadable < 100', () => {
      it('should return squared value', () => {
        expect(computeDownloadableScore(47)).to.equal(47 * 47)
      })
    })
  })

  describe('computeCatalogScore(catalog)', () => {
    describe('catalog with no metrics', () => {
      const catalog = {}
      it('should return 0', () => {
        expect(computeCatalogScore(catalog, currentDate)).to.equal(0)
      })
    })

    describe('catalog with totalCount = 0', () => {
      const catalog = {
        metrics: {
          datasets: { totalCount: 0 }
        }
      }
      it('should return 0', () => {
        expect(computeCatalogScore(catalog, currentDate)).to.equal(0)
      })
    })

    describe('perfect but old catalog', () => {
      const catalog = {
        metrics: {
          datasets: {
            totalCount: 1,
            partitions: {
              openness: { yes: 1 },
              download: { yes: 1 },
            }
          },
          mostRecentRevisionDate: new Date('2000-01-01')
        }
      }
      it('should return 0', () => {
        expect(computeCatalogScore(catalog, currentDate)).to.equal(100 * (100 * 100) * 1)
      })
    })

    describe('recent but poor catalog', () => {
      const catalog = {
        metrics: {
          datasets: {
            totalCount: 100,
            partitions: {
              openness: { yes: 11 },
              download: { yes: 12 },
            }
          },
          mostRecentRevisionDate: new Date('2016-05-01')
        }
      }
      it('should return 0', () => {
        expect(computeCatalogScore(catalog, currentDate)).to.equal(11 * (12 * 12) * 10)
      })
    })
  })

})
<commit_msg>Remove useless import<commit_after>import { getCandidateCatalogs, computeFreshnessScore, isObsolete, computeOpenScore, computeDownloadableScore, computeCatalogScore } from '../catalogs'

describe('catalogs', function () {
  const currentDate = new Date('2016-07-15')

  describe('getCandidateCatalogs(catalogs, sourceCatalogs)', function () {
    describe('catalog download or openness data is not defined', () => {
      it('should not return catalog', () => {
        const sourceCatalogs = ['1', '2', '3']
        const catalog = { id: '4', metrics: null }
        const catalogs = [catalog]

        expect(getCandidateCatalogs(catalogs, sourceCatalogs)).to.eql([])
      })
    })

    describe('catalog has no open or download data', () => {
      it('should not return catalog', () => {
        const sourceCatalogs = ['1', '2', '3']
        const catalog = { id: '4', metrics: { datasets: { partitions: { openness: { yes: 0 }, download: { yes: 0 } } } } }
        const catalogs = [catalog]

        expect(getCandidateCatalogs(catalogs, sourceCatalogs)).to.eql([])
      })
    })

    describe('catalog has at least one open and download data', () => {
      it('should return catalog', () => {
        const sourceCatalogs = ['1', '2', '3']
        const catalog = { id: '4', metrics: { datasets: { partitions: { openness: { yes: 42 }, download: { yes: 1 } } } } }
        const catalogs = [catalog]

        expect(getCandidateCatalogs(catalogs, sourceCatalogs)).to.eql([catalog])
      })
    })

    describe('catalog is already in catalogSources', () => {
      it('should not return catalog', () => {
        const sourceCatalogs = ['1', '2', '3']
        const catalog = { id: '1', metrics: { datasets: { partitions: { openness: { yes: 42 }, download: { yes: 1 } } } } }
        const catalogs = [catalog]

        expect(getCandidateCatalogs(catalogs, sourceCatalogs)).to.eql([])
      })
    })
  })

  describe('computeFreshnessScore(moment)', function () {
    describe('moment is not defined', function () {
      it('should return 0', function () {
        expect(computeFreshnessScore(null, currentDate)).to.equal(1)
      })
    })
    describe('moment is more recent than 1 month', function () {
      it('should return 100', function () {
        expect(computeFreshnessScore(new Date('2016-07-01'), currentDate)).to.equal(100)
      })
    })
    describe('moment is more recent than 6 months', function () {
      it('should return 10', function () {
        expect(computeFreshnessScore(new Date('2016-05-01'), currentDate)).to.equal(10)
      })
    })
    describe('moment is older than 6 months', function () {
      it('should return 0', function () {
        expect(computeFreshnessScore(new Date('2011-07-01'), currentDate)).to.equal(1)
      })
    })
  })

  describe('isObsolete(catalog)', () => {
    describe('catalog has no mostRecentRevisionDate metric', () => {
      const catalog = {}
      it('should return undefined', function () {
        return expect(isObsolete(catalog, currentDate)).to.be.undefined
      })
    })
    describe('mostRecentRevisionDate is more recent than 6 months', () => {
      const catalog = { metrics: { mostRecentRevisionDate: new Date('2016-06-01') } }
      it('should return false', function () {
        return expect(isObsolete(catalog, currentDate)).to.be.false
      })
    })
    describe('mostRecentRevisionDate is older than 6 months', () => {
      const catalog = { metrics: { mostRecentRevisionDate: new Date('2011-06-01') } }
      it('should return true', function () {
        return expect(isObsolete(catalog, currentDate)).to.be.true
      })
    })
  })

  describe('computeOpenScore(percentOpen)', () => {
    describe('percentOpen >= 100', () => {
      it('should return 100', () => {
        expect(computeOpenScore(500)).to.equal(100)
        expect(computeOpenScore(100)).to.equal(100)
      })
    })
    describe('percentOpen <= 1', () => {
      it('should return 1', () => {
        expect(computeOpenScore(-10)).to.equal(1)
        expect(computeOpenScore(0)).to.equal(1)
        expect(computeOpenScore(1)).to.equal(1)
      })
    })
    describe('1 < percentOpen < 100', () => {
      it('should return the same value', () => {
        expect(computeOpenScore(47)).to.equal(47)
      })
    })
  })

  describe('computeDownloadableScore(percentDownloadable)', () => {
    describe('percentDownloadable >= 100', () => {
      it('should return 100 * 100', () => {
        expect(computeDownloadableScore(500)).to.equal(100 * 100)
        expect(computeDownloadableScore(100)).to.equal(100 * 100)
      })
    })
    describe('percentDownloadable <= 1', () => {
      it('should return 1', () => {
        expect(computeDownloadableScore(-10)).to.equal(1)
        expect(computeDownloadableScore(0)).to.equal(1)
        expect(computeDownloadableScore(1)).to.equal(1)
      })
    })
    describe('1 < percentDownloadable < 100', () => {
      it('should return squared value', () => {
        expect(computeDownloadableScore(47)).to.equal(47 * 47)
      })
    })
  })

  describe('computeCatalogScore(catalog)', () => {
    describe('catalog with no metrics', () => {
      const catalog = {}
      it('should return 0', () => {
        expect(computeCatalogScore(catalog, currentDate)).to.equal(0)
      })
    })

    describe('catalog with totalCount = 0', () => {
      const catalog = {
        metrics: {
          datasets: { totalCount: 0 }
        }
      }
      it('should return 0', () => {
        expect(computeCatalogScore(catalog, currentDate)).to.equal(0)
      })
    })

    describe('perfect but old catalog', () => {
      const catalog = {
        metrics: {
          datasets: {
            totalCount: 1,
            partitions: {
              openness: { yes: 1 },
              download: { yes: 1 },
            }
          },
          mostRecentRevisionDate: new Date('2000-01-01')
        }
      }
      it('should return 0', () => {
        expect(computeCatalogScore(catalog, currentDate)).to.equal(100 * (100 * 100) * 1)
      })
    })

    describe('recent but poor catalog', () => {
      const catalog = {
        metrics: {
          datasets: {
            totalCount: 100,
            partitions: {
              openness: { yes: 11 },
              download: { yes: 12 },
            }
          },
          mostRecentRevisionDate: new Date('2016-05-01')
        }
      }
      it('should return 0', () => {
        expect(computeCatalogScore(catalog, currentDate)).to.equal(11 * (12 * 12) * 10)
      })
    })
  })

})
<|endoftext|>


<commit_before>import { useEffect, useMemo } from 'react';
import useExperimentParameters from 'hooks/useExperimentParameters';
import { activateOptimize } from 'utils/gtm';

export default companyName => {
  useEffect(() => {
    activateOptimize('testInterviewFormType');
  }, []);
  const experimentParameters = useExperimentParameters(['interviewFormType']);

  const link = useMemo(() => {
    if (experimentParameters.interviewFormType === '20200530-B-typeform') {
      if (companyName) {
        return { state: { share: 'interview', companyName } };
      } else {
        return { state: { share: 'interview' } };
      }
    } else {
      if (companyName) {
        return `/share/interview?companyName=${companyName}`;
      } else {
        return '/share/interview/step1';
      }
    }
  }, [companyName, experimentParameters.interviewFormType]);

  return link;
};
<commit_msg>hotfix: remove a/b testing (#938)<commit_after>export default companyName => {
  if (companyName) {
    return { state: { share: 'interview', companyName } };
  } else {
    return { state: { share: 'interview' } };
  }
};
<|endoftext|>


<commit_before>var _ = require('lodash');
var parallelStream = require('pelias-parallel-stream');
var peliasConfig = require( 'pelias-config' ).generate();
var regions = require('../data/regions');
var peliasLogger = require( 'pelias-logger' );
var getAdminLayers = require( './getAdminLayers' );

//defaults to nowhere
var optsArg = {
  transports: []
};
//only prints to suspect records log if flag is set
if (peliasConfig.logger.suspectFile === true){
  optsArg.transports.push(new peliasLogger.winston.transports.File( {
    filename: 'suspect_wof_records.log',
    timestamp: false
  }));
}

var logger = peliasLogger.get( 'wof-admin-lookup', optsArg );

regions.isSupported = function(country, name) {
  return this.hasOwnProperty(country) && this[country].hasOwnProperty(name);
};

regions.getCode = function(countries, regions) {
  if (_.isEmpty(countries) || _.isEmpty(regions)) {
    return undefined;
  }

  var country = countries[0].name;
  var region = regions[0].name;

  if (this.isSupported(country, region)) {
    return this[country][region];
  }

  return undefined;

};

function setFields(values, doc, wofFieldName, abbreviation) {
  try {
    if (!_.isEmpty(values)) {
      doc.addParent(wofFieldName, values[0].name, values[0].id.toString(), abbreviation);
    }
  }
  catch (err) {
    logger.info('invalid value', {
      centroid: doc.getCentroid(),
      result: {
        type: wofFieldName,
        values: values,
        abbreviation: abbreviation
      }
    });
  }
}

function hasCountry(result) {
  return _.isEmpty(result.country);
}

function hasAnyMultiples(result) {
  return Object.keys(result).some(function(element) {
    return result[element].length > 1;
  });
}

function createLookupStream(resolver, config) {

  if (!resolver) {
    throw new Error('createLookupStream requires a valid resolver to be passed in as the first parameter');
  }

  config = config || peliasConfig;

  var maxConcurrentReqs = 1;
  if (config.imports.adminLookup && config.imports.adminLookup.maxConcurrentReqs) {
    maxConcurrentReqs = config.imports.adminLookup.maxConcurrentReqs;
  }

  var stream = parallelStream(maxConcurrentReqs, function (doc, enc, callback) {

    // don't do anything if there's no centroid
    if (_.isEmpty(doc.getCentroid())) {
      return callback(null, doc);
    }

    resolver.lookup(doc.getCentroid(), function (err, result) {

      // assume errors at this point are fatal, so pass them upstream to kill stream
      if (err) {
        logger.error(err);
        return callback(new Error('PIP server failed: ' + (err.message || JSON.stringify(err))));
      }

      // log results w/o country OR any multiples
      if (hasCountry(result)) {
        logger.info('no country', {
          centroid: doc.getCentroid(),
          result: result
        });
      }
      if (hasAnyMultiples(result)) {
        logger.info('multiple values', {
          centroid: doc.getCentroid(),
          result: result
        });
      }

      var regionCode = regions.getCode(result.country, result.region);
      var countryCode = getCountryCode(result);

      // set code if available
      if (!_.isEmpty(countryCode)) {
        doc.setAlpha3(countryCode);
      }

      setFields(result.country, doc, 'country', countryCode);
      setFields(result.macroregion, doc, 'macroregion');
      if (!_.isEmpty(result.region)) { // if there are regions, use them
        setFields(result.region, doc, 'region', regionCode);
      } else { // go with dependency for region (eg - Puerto Rico is a dependency)
        setFields(result.dependency, doc, 'region');
      }
      setFields(result.macrocounty, doc, 'macrocounty');
      setFields(result.county, doc, 'county');
      setFields(result.locality, doc, 'locality');
      setFields(result.localadmin, doc, 'localadmin');
      setFields(result.borough, doc, 'borough');
      setFields(result.neighbourhood, doc, 'neighbourhood');

      callback(null, doc);
    }, getAdminLayers(doc.getLayer()));
  },
  function end() {
    if (typeof resolver.end === 'function') {
      resolver.end();
    }
  });

  return stream;
}

function getCountryCode(result) {
  if (result.country && result.country.length > 0 && result.country[0].hasOwnProperty('abbr')) {
    return result.country[0].abbr;
  }
  return undefined;
}

module.exports = {
  createLookupStream: createLookupStream
};
<commit_msg>add temp debug logging<commit_after>var _ = require('lodash');
var parallelStream = require('pelias-parallel-stream');
var peliasConfig = require( 'pelias-config' ).generate();
var regions = require('../data/regions');
var peliasLogger = require( 'pelias-logger' );
var getAdminLayers = require( './getAdminLayers' );

//defaults to nowhere
var optsArg = {
  transports: []
};
//only prints to suspect records log if flag is set
if (peliasConfig.logger.suspectFile === true){
  optsArg.transports.push(new peliasLogger.winston.transports.File( {
    filename: 'suspect_wof_records.log',
    timestamp: false
  }));
}

var logger = peliasLogger.get( 'wof-admin-lookup', optsArg );

regions.isSupported = function(country, name) {
  return this.hasOwnProperty(country) && this[country].hasOwnProperty(name);
};

regions.getCode = function(countries, regions) {
  if (_.isEmpty(countries) || _.isEmpty(regions)) {
    return undefined;
  }

  var country = countries[0].name;
  var region = regions[0].name;

  if (this.isSupported(country, region)) {
    return this[country][region];
  }

  return undefined;

};

function setFields(values, doc, wofFieldName, abbreviation) {
  try {
    if (!_.isEmpty(values)) {
      doc.addParent(wofFieldName, values[0].name, values[0].id.toString(), abbreviation);
    }
  }
  catch (err) {
    logger.info('invalid value', {
      centroid: doc.getCentroid(),
      result: {
        type: wofFieldName,
        values: values,
        abbreviation: abbreviation
      }
    });
  }
}

function hasCountry(result) {
  return _.isEmpty(result.country);
}

function hasAnyMultiples(result) {
  return Object.keys(result).some(function(element) {
    return result[element].length > 1;
  });
}

function createLookupStream(resolver, config) {

  if (!resolver) {
    throw new Error('createLookupStream requires a valid resolver to be passed in as the first parameter');
  }

  config = config || peliasConfig;

  var maxConcurrentReqs = 1;
  if (config.imports.adminLookup && config.imports.adminLookup.maxConcurrentReqs) {
    maxConcurrentReqs = config.imports.adminLookup.maxConcurrentReqs;
  }

  var stream = parallelStream(maxConcurrentReqs, function (doc, enc, callback) {

    // don't do anything if there's no centroid
    if (_.isEmpty(doc.getCentroid())) {
      return callback(null, doc);
    }

    resolver.lookup(doc.getCentroid(), function (err, result) {

      // assume errors at this point are fatal, so pass them upstream to kill stream
      if (err) {
        logger.error(err);
        return callback(new Error('PIP server failed: ' + (err.message || JSON.stringify(err))));
      }

      // log results w/o country OR any multiples
      if (hasCountry(result)) {
        logger.info('no country', {
          centroid: doc.getCentroid(),
          result: result
        });
      }
      if (hasAnyMultiples(result)) {
        logger.info('multiple values', {
          centroid: doc.getCentroid(),
          result: result
        });
      }

      var regionCode = regions.getCode(result.country, result.region);
      var countryCode = getCountryCode(result);

      // set code if available
      if (!_.isEmpty(countryCode)) {
        doc.setAlpha3(countryCode);
      }
      else {
        // TBD: remove this after debugging is done!!!
        logger.error('no country code', result);
      }

      setFields(result.country, doc, 'country', countryCode);
      setFields(result.macroregion, doc, 'macroregion');
      if (!_.isEmpty(result.region)) { // if there are regions, use them
        setFields(result.region, doc, 'region', regionCode);
      } else { // go with dependency for region (eg - Puerto Rico is a dependency)
        setFields(result.dependency, doc, 'region');
      }
      setFields(result.macrocounty, doc, 'macrocounty');
      setFields(result.county, doc, 'county');
      setFields(result.locality, doc, 'locality');
      setFields(result.localadmin, doc, 'localadmin');
      setFields(result.borough, doc, 'borough');
      setFields(result.neighbourhood, doc, 'neighbourhood');

      callback(null, doc);
    }, getAdminLayers(doc.getLayer()));
  },
  function end() {
    if (typeof resolver.end === 'function') {
      resolver.end();
    }
  });

  return stream;
}

function getCountryCode(result) {
  if (result.country && result.country.length > 0 && result.country[0].hasOwnProperty('abbr')) {
    return result.country[0].abbr;
  }
  return undefined;
}

module.exports = {
  createLookupStream: createLookupStream
};
<|endoftext|>


<commit_before>export const AVATAR_COLORS = [
	'FF004D', '00E756', 'FFF1E8', 'FF77A8', '00E756', 'FF004D', 'AB5236', 'FFFF27', 'FFFF27', '00E756',
	'FF77A8', '7E2553', '008751', '29ADFF', '1D2B53', 'FF004D', 'FF004D', '83769C', '83769C', '7E2553',
	'29ADFF', 'FFA300', 'FF004D', 'AB5236', 'FFFF27', 'FFFF27', 'FF004D', '29ADFF', 'FFA300', 'FF77A8',
	//	'29ADFF', 'FF004D', 'FF77A8', 'FFA300', 'AB5236', 'AB5236', '83769C', '83769C', '83769C', '83769C',
	//	'29ADFF', '00E756', '29ADFF', 'FF004D', '29ADFF', 'FFA300', '5F574F', '00E756', '00E756', '00E756',
]
export const TOTAL_AVATAR = AVATAR_COLORS.length
export const PRESERVED_AVATAR_COLORS = [
	'6ACAAD'
]
export let FINGERPRINT = (new Fingerprint2()).getSync().fprint
let videoPlayer
export const setVideoPlayer = player => videoPlayer = player
export const getVideoTime = () => videoPlayer && videoPlayer.getCurrentTime ? videoPlayer.getCurrentTime() : -1
export const getWindowWidth = () => window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth
export const getWindowHeight = () => window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight
export const VOTE_TIMEOUT = 27000
export const QUIZ_TIMEOUT = 20000
export const DISPLAY_TIMEOUT = 7000
export const preservedUsers = {
	system: {
		avatarSelected: 0,
		exp: 100,
		name: '系統',
		preserved: true
	}
}<commit_msg>working<commit_after>export const AVATAR_COLORS = [
	'FF004D', '00E756', 'FFF1E8', 'FF77A8', '00E756', 'FF004D', 'AB5236', 'FFFF27', 'FFFF27', '00E756',
	'FF77A8', '7E2553', '008751', '29ADFF', '1D2B53', 'FF004D', 'FF004D', '83769C', '83769C', '7E2553',
	'29ADFF', 'FFA300', 'FF004D', 'AB5236', 'FFFF27', 'FFFF27', 'FF004D', '29ADFF', 'FFA300', 'FF77A8',
	//	'29ADFF', 'FF004D', 'FF77A8', 'FFA300', 'AB5236', 'AB5236', '83769C', '83769C', '83769C', '83769C',
	//	'29ADFF', '00E756', '29ADFF', 'FF004D', '29ADFF', 'FFA300', '5F574F', '00E756', '00E756', '00E756',
]
export const TOTAL_AVATAR = AVATAR_COLORS.length
export const PRESERVED_AVATAR_COLORS = [
	'6ACAAD'
]
export let FINGERPRINT = (new Fingerprint2()).getSync().fprint
let videoPlayer
export const setVideoPlayer = player => videoPlayer = player
export const getVideoTime = () => videoPlayer && videoPlayer.getCurrentTime ? videoPlayer.getCurrentTime() : -1
export const getWindowWidth = () => window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth
export const getWindowHeight = () => window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight
export const VOTE_TIMEOUT = 27000
export const QUIZ_TIMEOUT = 20000
export const DISPLAY_TIMEOUT = 9000
export const preservedUsers = {
	system: {
		avatarSelected: 0,
		exp: 100,
		name: '系統',
		preserved: true
	}
}<|endoftext|>