Как лучше назвать и сделать?
От: пффф  
Дата: 19.06.23 03:54
Оценка:
Привет!

При формировании строки из каких-нибудь флагов, например, или ещё из чего-то, что требует разделителя между равнозначными частями, приходится проверять, первый ли это элемент, надо ли вставлять разделитель. Это как-то надоедает. Есть ли какой-то стандартный паттерн с реализованным класс для такого?

Я сначала написал функцию:
std::string appendWithSep(std::string str, const std::string &sAppend, const std::string &sSep = " ")
{
    if (sAppend.empty()) // Do nothing on empty append
        return str;

    if (str.empty())
    {
        str.append(sAppend);
    }
    else
    {
        str.append(sSep);
        str.append(sAppend);
    }

    return str;
}


Но она меня тоже стала утомлять — надо туда передавать строку, к которой добавляем, и разделитель. Написал классик:
lass StringAppendWithSep
{
    std::string  strSep;
    std::string  strBuf;

public:

    StringAppendWithSep(const std::string &sep="", const std::string &strOrg=std::string())
    : strSep(sep)
    , strBuf(strOrg)
    {}

    bool empty() const
    {
        return strBuf.empty();
    }

    std::string modifySep(std::string newSep)
    {
        std::swap(strSep, newSep);
        return newSep;
    }

    StringAppendWithSep& append(const std::string &s)
    {
        strBuf = appendWithSep(strBuf, s, strSep);
        return *this;
    }

    StringAppendWithSep& append(std::size_t n, char ch)
    {
        append(std::string(n, ch));
        return *this;
    }

    StringAppendWithSep& append(char ch)
    {
        append(1, ch);
        return *this;
    }

    StringAppendWithSep& concat(const std::string &s)
    {
        strBuf.append(s);
        return *this;
    }

    StringAppendWithSep& concat(std::size_t n, char ch)
    {
        strBuf.append(n, ch);
        return *this;
    }

    StringAppendWithSep& concat(char ch)
    {
        strBuf.append(1, ch);
        return *this;
    }

    std::string toString() const
    {
        return strBuf;
    }

};


Используется как-то так:
auto collector = StringAppendWithSep(", ");
for(auto s: list)
{
    if (optionalCondition)
        collector.append(s);
}

// или
if (v&Flag1)
    collector.append("Flag1");
if (v&Flag2)
    collector.append("Flag2");

std::cout << collector.str() << "\n";



В целом, функциональность устраивает, но название какое-то корявенькое. Может, как-то лучше можно назвать?

Ну, и вообще, может, как-то получше можно сделать?

ЗЫ Я не очень в си-шарпах и прочих явах разбираюсь, но там вроде есть всякие StringBuilder'ы. Вот это моё не имеет с ними сходства?
Отредактировано 19.06.2023 11:55 пффф . Предыдущая версия .
Re: Как лучше назвать и сделать?
От: sergii.p  
Дата: 19.06.23 07:42
Оценка:
Здравствуйте, пффф, Вы писали:

делал с помощью лямбды:

auto join(std::string separator) {
    return [sep = std::move(separator)](std::string res, const std::string& to_add) { 
        return res.empty() ? to_add : std::move(res) + sep + to_add; };
}


потом можно использовать в fold-ах в 23-ем (но у меня был самописный конечно)

std::ranges::fold_left(strings, std::string{}, join(", "));


со StringBuilder врядли много общего. В шарпах есть отдельный метод String.Join
Re[2]: Как лучше назвать и сделать?
От: Кодт Россия  
Дата: 19.06.23 09:51
Оценка: +1
Здравствуйте, sergii.p, Вы писали:

SP>делал с помощью лямбды:


SP>
SP>auto join(std::string separator) {
SP>    return [sep = std::move(separator)](std::string res, const std::string& to_add) { 
SP>        return res.empty() ? to_add : std::move(res) + sep + to_add; };
SP>}
SP>


auto j = join(",");
string acc = "";

acc = j(acc, "");
acc = j(acc, "");
acc = j(acc, "3");
acc = j(acc, "4");

Какой результат мы хотим увидеть? ",,3,4" или "3,4" ?
Перекуём баги на фичи!
Re[3]: Как лучше назвать и сделать?
От: sergii.p  
Дата: 19.06.23 11:21
Оценка:
Здравствуйте, Кодт, Вы писали:

К>Какой результат мы хотим увидеть? ",,3,4" или "3,4" ?


да, согласен. Невнимательно прочитал изначальный код.
Можно передавать дополнительный флажок по аналогии с Qt

enum class JoinBehaviour {
    SkipEmptyParts,
    KeepEmptyParts
};

auto join(std::string separator, JoinBehaviour method = JoinBehaviour::SkipEmptyParts){ ... }


Но мне хватало предыдущего варианта. Потому как всегда можно выфильтровать значения и потом уже соединять

strings
 | filter([](const auto& str) { return !str.empty(); })
 | join(", ");
Re[2]: Как лучше назвать и сделать?
От: пффф  
Дата: 19.06.23 11:55
Оценка:
Здравствуйте, sergii.p, Вы писали:

SP>делал с помощью лямбды:


SP>
SP>auto join(std::string separator) {
SP>    return [sep = std::move(separator)](std::string res, const std::string& to_add) { 
SP>        return res.empty() ? to_add : std::move(res) + sep + to_add; };
SP>}
SP>


А что так сложно? Я так сразу и не пойму, что тут вообще происходит.
У меня как-то так в итоге используется:
auto collector = StringAppendWithSep(", ");
for(auto s: list)
{
    collector.append(s);
}

// или
if (v&Flag1)
    collector.append("Flag1");
if (v&Flag2)
    collector.append("Flag2");

std::cout << collector.str() << "\n";




SP>со StringBuilder врядли много общего. В шарпах есть отдельный метод String.Join


Join сливает вместе несколько переданных значений, или содержимое контейнера? Не совсем то. Мне либо в цикле обычно надо, с каким-то условием, и конечно наверное правильно бы делать std::transform, foreach и тп, но я как-то попроще привык. Ну, или флаги какие проверить, и сделать текстовое представление для них. В принципе, флаги тоже можно в контейнер загнать, и строки для флагов рядом, и всё в цикле, но если надо один раз прямо тут и всё — то это какой-то оверхед
Re[3]: Как лучше назвать и сделать?
От: sergii.p  
Дата: 19.06.23 12:32
Оценка:
Здравствуйте, пффф, Вы писали:

П>А что так сложно? Я так сразу и не пойму, что тут вообще происходит.

П>У меня как-то так в итоге используется:
П>
П>auto collector = StringAppendWithSep(", ");
П>for(auto s: list)
П>{
П>    collector.append(s);
П>}

П>// или
П>if (v&Flag1)
П>    collector.append("Flag1");
П>if (v&Flag2)
П>    collector.append("Flag2");

П>std::cout << collector.str() << "\n";
П>


оно не сложно, оно по-другому. Я делал упор на функциональщину и неизменяемость. Для ФП каррировать какие-то функции в порядке вещей.
Подобный код я реализовывал как-то так:

enum class Mask {...};

enum class Flag {...};

MaskIter split (const Mask m) {...}

std::string_view to_string(const Flag f) {...}

split(mask) // разделяет битовое поле на элементы
    | transform(to_string) 
    | join(", ")


В проекте Mask и Flag подчинялись единым требованиям. Поэтому можно было сделать общую функцию split.
А так конечно на любителя. Лично мне решения с мутабельным StringBuilder не нравятся. Особенно когда кто-то посреди веселья захочет вызвать modifySep.
Re: imploder
От: Sm0ke Россия ksi
Дата: 19.06.23 23:09
Оценка:
Здравствуйте, пффф, Вы писали:

П>Привет!


Я бы назвал такой класс impoder
В честь функции implode() из php (обратное от explode() aka split)

gbolt: https://godbolt.org/z/dKvePffc4

Реализовано через stringstream

// Имплементация:

#include <concepts>
#include <sstream>
#include <string>
#include <cstdint>

enum class empty_kind { skip, accept };

template <empty_kind Empty_kind>
class imploder {
    // data
    std::stringstream
        m_stream;
    std::string
        m_separator;
    std::uintptr_t
        m_length{0};

public:
    using t_text = const std::string &;

    imploder(t_text p_separator) : m_separator{p_separator} {}
    imploder(std::string && p_separator) : m_separator{std::move(p_separator)} {}

    void append(t_text p_str) {
        if constexpr ( Empty_kind == empty_kind::skip ) {
            if( p_str.empty() ) { return; }
        }
        if( m_length > 0 ) {
            m_stream << m_separator;
            m_length += m_separator.length();
        }
        m_stream << p_str;
        m_length += p_str.length();
    }

    template <std::same_as<std::string> ... Args>
    void append(const Args & ... p_items) {
        (append(p_items), ...);
    }

    template <typename Container>
    void append_range(const Container & p_items) {
        for( typename Container::const_reference v_it : p_items ) {
            append(v_it);
        }
    }

    std::string result() const { return m_stream.str(); }
};

// Использование:

#include <vector>
#include <iostream>

int main() {
    using namespace std::string_literals;
    
    // imploder<empty_kind::accept> v_imp{", "s};
    imploder<empty_kind::skip> v_imp{", "s};
    v_imp.append("a"s, "b"s, ""s, "c"s);

    std::vector<std::string> v_vec{"d"s, ""s, "e"s, "f"s};
    v_imp.append_range(v_vec);

    std::cout << v_imp.result() << '\n';
    return 0;
}
Отредактировано 19.06.2023 23:16 Sm0ke . Предыдущая версия . Еще …
Отредактировано 19.06.2023 23:12 Sm0ke . Предыдущая версия .
Re: Как лучше назвать и сделать?
От: VVV Россия  
Дата: 20.06.23 00:00
Оценка: +4
Здравствуйте, пффф, Вы писали:

П>При формировании строки из каких-нибудь флагов, например, или ещё из чего-то, что требует разделителя между равнозначными частями, приходится проверять, первый ли это элемент, надо ли вставлять разделитель. Это как-то надоедает. Есть ли какой-то стандартный паттерн с реализованным класс для такого?


Видел/использовал такой 'паттерн':

string sep = ""; //пустая строка(или что-то другое, например "\t\t\t"), тут по желанию string, string_view, const char *, char, ... генератор разделителей
string result;
for (const auto& sflag : containerFlags)
{
    result += sep;
    result += sflag;

    sep = ", ";
}
Re: Как лучше назвать и сделать?
От: K13 http://akvis.com
Дата: 21.06.23 05:28
Оценка:
Здравствуйте, пффф, Вы писали:

П>При формировании строки из каких-нибудь флагов, например, или ещё из чего-то, что требует разделителя между равнозначными частями, приходится проверять, первый ли это элемент, надо ли вставлять разделитель. Это как-то надоедает. Есть ли какой-то стандартный паттерн с реализованным класс для такого?


Я бы сделал проще:

class separated
{
public:
  enum class Empty { Skip, Add };
  // при желании добавить конструкторы с char / string_view
  explicit separated( const std::string& separator, Empty e = Empty::Skip ): sep(separator), empty(e) {}

  separated& operator<<( std::string_view s )
  {
    if ( !s.empty() || empty == Empty::Add )
    {
      if ( !first )
        buffer.append( sep );
      buffer.append( s );
      first = false;
    }
    return *this;
  }

  const std::string& as_string() const & { return buffer; }
  std::string as_string() const && { return std::move(buffer); }
private:
  std::string sep;
  std::string buffer;
  Empty empty;
  bool first = true;
};

и использование:
template< typename... Args >
std::string join_separated( std::string sep, Args&&... args )
{
    separated s(sep);
    (( s << args ),...);
    return std::move(s).as_string();
}

int main()
{
    const char *arr[] = { "", "", "Hello", "World", "", "!!!" };
    separated sep(", ", separated::Empty::Add );
    for ( auto s: arr )
      sep << s;
    std::cout << "loop ver arr[] -> " << sep.as_string() << std::endl;
    
    std::cout << "join -> " << join_separated( "-", std::string{"alpha"}, std::string_view{"beta"}, "gamma" ) << std::endl;
    
    return 0;
}

http://coliru.stacked-crooked.com/a/a6fb2d6d4ccd4793
Re: Как лучше назвать и сделать?
От: Sergey_BG Россия  
Дата: 21.06.23 09:09
Оценка:
Здравствуйте, пффф, Вы писали:

Можно переписать код под использование ostream_iterator. Там уже реализован delimeter.
Сергей
Re[2]: Как лучше назвать и сделать?
От: пффф  
Дата: 21.06.23 09:11
Оценка:
Здравствуйте, Sergey_BG, Вы писали:

S_B>Можно переписать код под использование ostream_iterator. Там уже реализован delimeter.


Optional delimiter string is written to the output stream after every write operation.


Логика не совсем та, что нужна
Re: Как лучше назвать и сделать?
От: kov_serg Россия  
Дата: 21.06.23 12:42
Оценка:
Здравствуйте, пффф, Вы писали:

П>Привет!


П>При формировании строки из каких-нибудь флагов, например, или ещё из чего-то, что требует разделителя между равнозначными частями, приходится проверять, первый ли это элемент, надо ли вставлять разделитель. Это как-то надоедает. Есть ли какой-то стандартный паттерн с реализованным класс для такого?

...
П>Ну, и вообще, может, как-то получше можно сделать?

Вот так можно:
enum RGB { R=1,G=2,B=4 };
template<class L>void write_flags(RGB f,L addif) { 
    addif(f&R,"R");
    addif(f&G,"G");
    addif(f&B,"B");
}

enum Bits { B0=1,B1=2,B2=4,B3=8 };
template<class L>void write_flags(Bits f,L addif) {
    addif(f&B0,"B0");
    addif(f&B1,"B1");
    addif(f&B2,"B2");
    addif(f&B3,"B3");
}

пример использования
#include <iostream>
template<class F> struct Flags {
    F value; const char* sep; bool first;
    Flags(F value,const char* sep=",")    : value(value), sep(sep), first(true) {}
    Flags(int value,const char* sep=",") : value((F)value), sep(sep), first(true) {}
    friend std::ostream& operator<< (std::ostream &s,Flags<F> f) {
        write_flags(f.value,[&s,&f](bool cond,const char* msg) {
            if (!cond) return;
            if (f.first) f.first=false; else s<<f.sep;
            s<<msg;
        });
        return s;
    }
};
int main(int argc, char const *argv[]) {
    Bits bits=(Bits)(B0|B3);
    std::cout<< "RGB={" << Flags<RGB>( R|G ) <<"} "
        << "bits={" << Flags(bits) << "}" << std::endl;
    return 0;
}

RGB={R,G} bits={B0,B3}
Re: Как лучше назвать и сделать?
От: Кодт Россия  
Дата: 24.06.23 22:50
Оценка:
Здравствуйте, пффф, Вы писали:

П>При формировании строки из каких-нибудь флагов, например, или ещё из чего-то, что требует разделителя между равнозначными частями, приходится проверять, первый ли это элемент, надо ли вставлять разделитель. Это как-то надоедает. Есть ли какой-то стандартный паттерн с реализованным класс для такого?


Прямо совсем стандартных нет, хотя можно наделать из готовых алгоритмов или диапазонов

П>В целом, функциональность устраивает, но название какое-то корявенькое. Может, как-то лучше можно назвать?


В разных языках это по-разному называется, тут уже накидали вариантов.
Так что называй как хочешь, лишь бы самому было понятно и приятно.

П>Ну, и вообще, может, как-то получше можно сделать?


П>ЗЫ Я не очень в си-шарпах и прочих явах разбираюсь, но там вроде есть всякие StringBuilder'ы. Вот это моё не имеет с ними сходства?


А это зависит от того, как хочется обращаться.

// поштучно вызывать - нужен объект с состоянием
ListBuilder b(",");
b(first);
b(second);
b(third);
return (string)b;

// применять к контейнеру / диапазону / паре итераторов - можно состояние засунуть в функцию
vector<string> vs = {first, second, third};
return build_separated_list(",", vs);
return build_separated_list(",", begin(vs), end(vs));


В первом случае, очевидно, у объекта есть параметр "чем чередовать" и состояние: аккумулятор и "добавлен ли первый"
struct ListBuilder {
  std::string separator;  // или string_view, но тогда нужно следить за временем жизни
  std::string empty_case = "";

  std::ostringstream ost;  // string builder
  bool added = false;

  auto& operator()(auto&& elem) {
    if (added) ost << separator;
    ost << std::forward<decltype(elem)>(elem);
    added = true;
    return *this;
  }

  /*explicit*/ operator std::string() const {
    if (!added) {
      return empty_case; // либо кидаться ошибкой
    }
    return ost.str();
  }
};


Во втором случае — всё ещё проще, состояние определяется местом в коде
template<class It>
void print_separated_list(std::ostream& ost, std::string_view separator, It begin, It end) {
  if (begin == end) {
    // return либо ошибка
  }
  ost << *begin;
  ++begin;
  for ( ; begin != end; ++begin) ost << separator << *begin;
}

template<class It>
std::string build_separated_list(std::string_view separator, It begin, It end) {
  std::ostringstream ost;
  print_separated_list(ost, separator, begin, end);
  return ost.str();
}
Перекуём баги на фичи!
Re: Как лучше назвать и сделать?
От: McQwerty Россия  
Дата: 29.06.23 12:48
Оценка:
Здравствуйте, пффф, Вы писали:

П>При формировании строки из каких-нибудь флагов, например, или ещё из чего-то, что требует разделителя между равнозначными частями, приходится проверять, первый ли это элемент, надо ли вставлять разделитель. Это как-то надоедает. Есть ли какой-то стандартный паттерн с реализованным класс для такого?


Думаю, можно и не проверять, а всегда добавлять.

#include <string>
#include <iostream>

struct builder_t
{
    builder_t (const std::string& sep):
        sep_ (sep)
    {

    }

    builder_t& operator << (const std::string& v)
    {
        acc_ += sep_ + v;
        return * this;
    }

    std::string accumulated () const
    {
        return acc_. substr (sep_. length ());
    }

    const std::string sep_;
    std::string acc_;
};

int main (int, char**)
{
    builder_t builder (", ");

    builder << "flag1" << "flag2" << "flag3";

    std::cout << "{" << builder. accumulated () << "}" << std::endl;

    return 0;
} // main


Вроде, работает.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.