1

There was a problem to slightly change the macro described in Expandable macro that extracts the first character of UTF-8/cyrillic string without additional packages for the argument which may contain two or more words. That is, for example, for the argument Vladimir Fedorovich it would return V.F., and for Владимир Фёдорович - В.Ф. Here is a working example by the highly respected David Carlisle for a one-word argument (of course it can contain many words, but only the first letter of the first word will be extracted):

\documentclass{article}

\usepackage[T2A]{fontenc}
\usepackage[utf8]{inputenc}
\usepackage[russian]{babel}

\makeatletter
\newcommand{\firstof}[1]{\expandafter\checkfirst#1\@nil}
\def\checkfirst#1{%
  \ifx\UTFviii@two@octets#1%
  \expandafter\gettwooctets
  \else
  \expandafter\@car\expandafter#1%
  \fi
}
\def\gettwooctets#1#2#3\@nil{\UTFviii@two@octets#1#2}

\makeatother

\begin{document}

\firstof{Vladimir}

\firstof{Владимир}

\end{document}

I need the \firstof{Vladimir Fedorovich} macro call to return V.F. and the call \firstof{Владимир Фёдорович} - В.Ф.. But theoretically, there can be more than 2 words in the argument.

I will be grateful for help.

2 Answers 2

7

The following does what you want.

I coded it in L3 instead of on the low level David used. The basic idea is still the same, if it starts with something that expands to \UTFviii@two@octets we collect those two octets, else we just use the first character. However, this also uses \text_purify:n on the input to ensure the input to the loop is only text.

Then we simply split at each space (we have to check whether we're done with \quark_if_recursion_tail_stop:n) and get our first character (or the two that make up the UTF8 octet pair).

The macro \firstofwords takes as its first argument the stuff it should put after each first letter of each word, and the second argument is the list of words separated by spaces.

\documentclass{article}

\usepackage[T2A]{fontenc}
\usepackage[russian]{babel}

\makeatletter
\newcommand{\firstof}[1]{\expandafter\checkfirst#1\@nil}
\def\checkfirst#1{%
  \ifx\UTFviii@two@octets#1%
  \expandafter\gettwooctets
  \else
  \expandafter\@car\expandafter#1%
  \fi
}
\def\gettwooctets#1#2#3\@nil{\UTFviii@two@octets#1#2}

\ExplSyntaxOn
\cs_new:Npn \crosfield_first_of_words:nn #1#2
  { \exp_not:e { \__crosfield_first_of_words:e { \text_purify:n {#2} } {#1} } }
\group_begin:
\cs_set:Npn \__crosfield_tmp:n #1
  {
    \cs_new:Npn \__crosfield_first_of_words:n ##1 ##2
      {
        \__crosfield_first_of_words_spaces:nw {##2} ##1 #1 % #1 is a space
          \q_recursion_tail #1 % #1 is a space
          \q_recursion_stop
      }
  }
\__crosfield_tmp:n { ~ }
\group_end:
\cs_generate_variant:Nn \__crosfield_first_of_words:n { e }
\makeatletter
\cs_new:Npn \__crosfield_first_of_words_spaces:nw #1 #2 ~
  {
    \quark_if_recursion_tail_stop:n {#2}
    \tl_if_head_eq_meaning:oNTF {#2} \UTFviii@two@octets
      { \__crosfield_first_of_words_aux:nnw #2 \q_stop }
      { \tl_head:n {#2} }
    \exp_not:n {#1}
    \__crosfield_first_of_words_spaces:nw {#1}
  }
\makeatother
\cs_generate_variant:Nn \tl_if_head_eq_meaning:nNTF { o }
% there can't be a \q_stop in the argument as that would've caused an infinite
% loop in \text_purify:n, so this is fine and faster than allowing arbitrary
% contents.
\cs_new:Npn \__crosfield_first_of_words_aux:nnw #1#2#3 \q_stop
  { \exp_not:n { #1#2 } }

% define a LaTeX2e name for the above function
\cs_new_eq:NN \firstofwords \crosfield_first_of_words:nn
\ExplSyntaxOff

\begin{document}

\firstof{Vladimir}

\firstof{Владимир}

\firstofwords{.\ }{Vladimir Владимир} example

\firstofwords{.}{foo bar baz}

\end{document}

enter image description here


Edit: If you only need the second argument and want to put a . inbetween the first letters every time you can use the following instead of \cs_new_eq:NN \firstofwords \crosfield_first_of_words:nn:

\newcommand* \firstofwords { \crosfield_first_of_words:nn {.} }

Edit2:

This variant only places the first argument to \firstofwords between elements, but not after the last one.

\documentclass[varwidth,border=3.14]{standalone}

\usepackage[T2A]{fontenc}
\usepackage[russian]{babel}

\makeatletter
\newcommand{\firstof}[1]{\expandafter\checkfirst#1\@nil}
\def\checkfirst#1{%
  \ifx\UTFviii@two@octets#1%
  \expandafter\gettwooctets
  \else
  \expandafter\@car\expandafter#1%
  \fi
}
\def\gettwooctets#1#2#3\@nil{\UTFviii@two@octets#1#2}

\ExplSyntaxOn
\cs_new:Npn \crosfield_first_of_words:nn #1#2
  { \exp_not:e { \__crosfield_first_of_words:e { \text_purify:n {#2} } {#1} } }
\group_begin:
\cs_set:Npn \__crosfield_tmp:n #1
  {
    \cs_new:Npn \__crosfield_first_of_words:n ##1 ##2
      {
        \__crosfield_first_of_words_spaces:Nnw \use_none:n {##2} ##1 #1 % #1 is a space
          \q_recursion_tail #1 % #1 is a space
          \q_recursion_stop
      }
  }
\__crosfield_tmp:n { ~ }
\group_end:
\cs_generate_variant:Nn \__crosfield_first_of_words:n { e }
\makeatletter
\cs_new:Npn \__crosfield_first_of_words_spaces:Nnw #1 #2 #3 ~
  {
    \quark_if_recursion_tail_stop:n {#3}
    #1 {#2}
    \tl_if_head_eq_meaning:oNTF {#3} \UTFviii@two@octets
      { \__crosfield_first_of_words_aux:nnw #3 \q_stop }
      { \tl_head:n {#3} }
    \__crosfield_first_of_words_spaces:Nnw \exp_not:n {#2}
  }
\makeatother
\cs_generate_variant:Nn \tl_if_head_eq_meaning:nNTF { o }
% there can't be a \q_stop in the argument as that would've caused an infinite
% loop in \text_purify:n, so this is fine and faster than allowing arbitrary
% contents.
\cs_new:Npn \__crosfield_first_of_words_aux:nnw #1#2#3 \q_stop
  { \exp_not:n { #1#2 } }

% define a LaTeX2e name for the above function
\cs_new_eq:NN \firstofwords \crosfield_first_of_words:nn
\ExplSyntaxOff

\begin{document}

\firstof{Vladimir}

\firstof{Владимир}

:\firstofwords{.\,}{Vladimir Владимир}.:

\firstofwords{.}{foo bar baz}

\end{document}

If you want to always use .\, between elements and a . after the last one you can use

\newcommand\firstofwords[1]{ \crosfield_first_of_words:nn {.\,} {#1} . }

Instead of the \cs_new_eq:NN-line.

8
  • Thanks for the fine code! But it seems that the first argument to the \firstofwords function is not needed. There should always be a dot char and in this sense it does not need to be passed inside the function.
    – Crosfield
    Jul 9 at 21:13
  • Thank you Skillmon! This is almost exactly what I need. Can you say one more thing pls? Now if I add a dot and a small non-breaking space as an argument to the \crosfield_first_of_words:nn {.\,} command, these characters will be added after each initial, but I need the .\, to be added only between the initials. Only a dot is needed at the end of the result (like A.\,F.). Is it possible to do this?
    – Crosfield
    Jul 10 at 9:53
  • @Crosfield just add an \unskip after the last one to remove it: \firstofwords{.\,}{Some string here}\unskip. If you don't like to add \unskip for this I could create some other code, but that needs time (and I don't have much time today)
    – Skillmon
    Jul 10 at 11:13
  • Unfortunately \unskip trick doesn't quite work. It's easier to put a negative space \! after \q_recursion_stop. Although this is of course also just dirty trick.
    – Crosfield
    Jul 10 at 13:40
  • @Crosfield oh, sorry my bad, \, uses \kern in text mode, so can't be \unskipped.
    – Skillmon
    Jul 11 at 16:23
3

With expl3 there is \text_map_inline:nn that can do what you want. The idea is to just break the mapping after the first item, adding the desired period.

\documentclass{article}

\usepackage[T2A]{fontenc}
\usepackage[utf8]{inputenc}
\usepackage[russian]{babel}

\ExplSyntaxOn

\NewDocumentCommand{\firstof}{m}
 {
  \crosfield_firstof:n { #1 }
 }

\seq_new:N \l__crosfield_firstof_items_seq

\cs_new_protected:Nn \crosfield_firstof:n
 {
  % split the input at spaces
  \seq_set_split:Nnn \l__crosfield_firstof_items_seq { ~ } { #1 }
  % extract the first item and add a period
  \seq_map_function:NN \l__crosfield_firstof_items_seq \__crosfield_firstof:n
 }

\cs_new_protected:Nn \__crosfield_firstof:n
 {% map the elements, breaking after the first
  \text_map_inline:nn { #1 } { ##1 \text_map_break:n { . } }
 }

\ExplSyntaxOff

\begin{document}

\firstof{Vladimir}

\firstof{Владимир}

\firstof{Vladimir Fedorovich} 

\firstof{Владимир Фёдорович}

\end{document}

enter image description here

Borrowing some code from Skillmon's answer we can get expandability.

\documentclass{article}

\usepackage[T2A]{fontenc}
\usepackage[utf8]{inputenc}
\usepackage[russian]{babel}

\ExplSyntaxOn

\NewExpandableDocumentCommand{\firstof}{m}
 {
  \crosfield_firstof:n { #1 }
 }

\cs_new:Npn \crosfield_firstof:n #1
 {
  \exp_not:e { \__crosfield_firstof:n { #1 } }
 }
\group_begin:
\cs_set:Npn \__crosfield_tmp:n #1
 {
  \cs_new:Nn \__crosfield_firstof:n
   {
    \__crosfield_firstof_item:w ##1 #1 % #1 is a space
    \q_recursion_tail #1 % #1 is a space
    \q_recursion_stop
   }
 }
\__crosfield_tmp:n { ~ }
\group_end:
\cs_generate_variant:Nn \__crosfield_firstof:n { e }

\cs_new:Npn \__crosfield_firstof_item:w #1 ~
 {
  \quark_if_recursion_tail_stop:n { #1 }
  \text_map_function:nN { #1 } \__crosfield_firstof_do:n 
  \__crosfield_firstof_item:w
 }

\cs_new:Nn \__crosfield_firstof_do:n { #1 \text_map_break:n { . } }

\ExplSyntaxOff

\begin{document}

\firstof{Vladimir}

\firstof{Владимир}

\firstof{Vladimir Fedorovich}

\firstof{Владимир Фёдорович}

\edef\test{\firstof{Владимир Фёдорович}}

\show\test

\end{document}

The \show command will print

> \test=macro:
->В.Ф..

on the console, witnessing the full expandability.

3
  • I suspect over the next few days we might end up with something indexed ;)
    – Joseph Wright
    Jul 12 at 19:02
  • 1
    @JosephWright I'll have a beer, in the meantime. Maybe a few beers.
    – egreg
    Jul 12 at 19:29
  • @egreg absolutely pro the beer part, cheers! I was considering using the \text_map... internals, but don't ask me why I decided against it... Maybe because of the space-splitting part? :)
    – Skillmon
    Jul 12 at 21:03

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .