xaizek / dit (License: GPLv3) (since 2018-12-07)
Command-line task keeper that remembers all old values and is meant to combine several orthogonal features to be rather flexible in managing items.
<root> / src / IdGenerator.cpp (6b2762725669880c5a6e869fdcc70a735b15ee7c) (5,647B) (mode 100644) [raw]
// Copyright (C) 2015 xaizek <xaizek@posteo.net>
//
// This file is part of dit.
//
// dit is free software: you can redistribute it and/or modify
// it under the terms of version 3 of the GNU Affero General Public
// License as published by the Free Software Foundation.
//
// dit is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with dit.  If not, see <http://www.gnu.org/licenses/>.

// Initial width of an ID is three.  It starts with count zero for ID sequence
// of width three.  Each time end of current sequence is reached, the width is
// incremented by one and count is reset back to zero.

#include "IdGenerator.hpp"

#include <cassert>
#include <cmath>

#include <algorithm>
#include <array>
#include <random>
#include <string>
#include <tuple>
#include <utility>

#include "Config.hpp"

static int getIdx(int k, int b);
template <typename C, typename G = std::mt19937>
inline C shuffle(C c, G &&g = std::mt19937(std::random_device{}()));

void
IdGenerator::init(Config &config, const std::string &alphabet)
{
    // Initial number of sequences (width of an ID) is three.
    std::mt19937 g(std::random_device{}());
    std::array<std::string, 3> sequences = {
        { shuffle(alphabet, g), shuffle(alphabet, g), shuffle(alphabet, g) }
    };

    config.set("!ids.sequences.alphabet", alphabet);
    config.set("!ids.sequences.count", std::to_string(sequences.size()));
    config.set("!ids.sequences.0", sequences[0]);
    config.set("!ids.sequences.1", sequences[1]);
    config.set("!ids.sequences.2", sequences[2]);
    config.set("!ids.count", "0");
    config.set("!ids.total", "0");
    config.set("!ids.next",
               { sequences[0][0], sequences[1][0], sequences[2][0] });
}

IdGenerator::IdGenerator(Config &config) : config(config)
{
}

std::string
IdGenerator::getId()
{
    ensureLoaded();

    return nextId;
}

void
IdGenerator::advanceId()
{
    ensureLoaded();

    std::tie(nextId, count) = advance(nextId, count);
    ++total;

    markModified();
}

void
IdGenerator::load()
{
    alphabet = config.get("!ids.sequences.alphabet");
    const int nseq = std::stoi(config.get("!ids.sequences.count"));
    for (int seq = 0; seq < nseq; ++seq) {
        sequences.push_back(
            config.get("!ids.sequences." + std::to_string(seq)));
    }

    nextId = config.get("!ids.next");
    count = std::stoi(config.get("!ids.count"));
    total = std::stoi(config.get("!ids.total"));
}

std::pair<std::string, int>
IdGenerator::advance(std::string id, int count)
{
    const int i = getIdx(count + 1, sequences[0].size());
    if (static_cast<unsigned int>(i) == id.length()) {
        // Theoretically we could pass in random number generator in the same
        // state it was used last time (e.g. after initialization), but this
        // step is so rare that it won't make any difference.
        sequences.push_back(shuffle(alphabet));

        // Make id from first characters of all sequences and reset count to
        // zero.
        id.clear();
        for (const std::string &seq : sequences) {
            id.push_back(seq.front());
        }
        return { std::move(id), 0 };
    }

    std::string::size_type x = sequences[i].find(id[i]);
    assert(x != std::string::npos && "Wrong character in id.");
    x = (x + 1)%sequences[0].size();
    id[i] = sequences[i][x];
    return { std::move(id), count + 1 };
}

/**
 * @brief Calculates number of the field to advance to get the next id.
 *
 * @param k Sequential number of the ID (base 1).
 * @param b Number base (number of "digits" per field).
 *
 * @returns The field number.
 */
static int
getIdx(int k, int b)
{
    // No check for k == 0 followed by returning zero as we never have such
    // input.

    int p = std::ceil(std::log(k)/std::log(b));
    int i = std::pow(b, p);
    while (p > 0) {
        if (k % i == 0) {
            break;
        }
        i /= b;
        --p;
    }
    return p;
}

/**
 * @brief Shuffles elements of the given container.
 *
 * @tparam C Type of the container.
 * @tparam G Type of random number generator.
 *
 * @param c Container itself.
 * @param g Generator.
 *
 * @returns Copy of the container with shuffled items.
 */
template <typename C, typename G>
inline C shuffle(C c, G &&g)
{
    std::shuffle(c.begin(), c.end(), g);
    return c;
}

void
IdGenerator::forEachId(std::function<void(const std::string &)> visitor)
{
    ensureLoaded();

    Config idsConfig("forEachId");
    dump(idsConfig);

    // Reset to initial state.
    idsConfig.set("!ids.next",
                  { sequences[0][0], sequences[1][0], sequences[2][0] });
    idsConfig.set("!ids.count", "0");
    idsConfig.set("!ids.total", "0");

    IdGenerator idsGenerator(idsConfig);
    for (int i = 0; i < total; ++i) {
        visitor(idsGenerator.getId());
        idsGenerator.advanceId();
    }
}

void
IdGenerator::save()
{
    if (isModified()) {
        dump(config);
    }
}

void
IdGenerator::dump(Config &config) const
{
    config.set("!ids.sequences.alphabet", alphabet);
    config.set("!ids.next", nextId);
    config.set("!ids.count", std::to_string(count));
    config.set("!ids.total", std::to_string(total));

    config.set("!ids.sequences.count", std::to_string(sequences.size()));
    for (unsigned int seq = 0U; seq < sequences.size(); ++seq) {
        config.set("!ids.sequences." + std::to_string(seq), sequences[seq]);
    }
}
Hints

Before first commit, do not forget to setup your git environment:
git config --global user.name "your_name_here"
git config --global user.email "your@email_here"

Clone this repository using HTTP(S):
git clone https://code.reversed.top/user/xaizek/dit

Clone this repository using ssh (do not forget to upload a key first):
git clone ssh://rocketgit@code.reversed.top/user/xaizek/dit

You are allowed to anonymously push to this repository.
This means that your pushed commits will automatically be transformed into a pull request:
... clone the repository ...
... make some changes and some commits ...
git push origin master