1
\$\begingroup\$

Context:

I'm building a React app using Ramda as a helper library for functional programming.

Users can import contacts into the app. These contacts can be vCards.

I've written a function that:

  1. Removes duplicates,
  2. Removes companies,
  3. Transforms the vcards data into contacts the React app understands.

The apps contacts look like this:

{
  firstName: 'Bob',
  lastName: 'Mueller',
  company: 'X-Enterprises',
  position: 'CEO',
  contactDetails: 'Cell phone: 555-55-5555 Email: bob.mueller@XE.com',
  conversationTopics: 'We talked about politics.'
  city: 'New York'
}

So an empty contact looks like this:

const emptyContact = {
  firstName: '',
  lastName: '',
  company: '',
  position: '',
  contactDetails: '',
  conversationTopics: ''
  city: ''
}

I will give you the fixtures with which I test my function in the end, because they are very long. But here is the test that I wrote to test my function:

describe('createVCardContacts()', async assert => {
  assert({
    given: 'an array of vcard contacts',
    should: 'return an array of valid contacts',
    actual: createVCardContacts(vCardData),
    expected: parsedVCardData,
  });
});

Valid contacts here means no duplicates and no companies.

Function:

So, here is the createVCardContacts function:

import * as R from 'ramda';
import * as RA from 'ramda-adjunct';
import * as R_ from 'ramda-extension';

// VCard data always has its value at index 3 and can either be
// an array or a value
const getDataFromVCard = R.pipe(
  R.ifElse(
    R.pipe(
      R.head,
      R_.isArray
    ),
    R.head,
    R.identity
  ),
  R.nth(3)
);
export const nGetter = R.pipe(
  getDataFromVCard,
  R.takeWhile(R.complement(R.isEmpty))
);
export const orgGetter = R.pipe(
  getDataFromVCard,
  R.replace(';', '')
);
export const bdayGetter = R.pipe(
  getDataFromVCard,
  R.concat('Birthday: ')
);
export const titleGetter = getDataFromVCard;
export const noteGetter = getDataFromVCard;
export const adrGetter = R.pipe(
  getDataFromVCard,
  R.nth(3)
);
export const telGetter = R.pipe(
  getDataFromVCard,
  R.concat('Cell phone: ')
);
export const emailGetter = R.pipe(
  getDataFromVCard,
  R.concat('Email: ')
);
const vCardEvolveObject = {
  n: nGetter,
  org: orgGetter,
  bday: bdayGetter,
  title: titleGetter,
  note: noteGetter,
  adr: adrGetter,
  tel: telGetter,
  email: emailGetter,
};
export const createVCardContacts = R.pipe(
  // Remove duplicates by name
  R.uniqBy(R.prop('n')),
  // Remove companies
  R.reject(
    R.both(
      R.both(R.has('fn'), R.has('org')),
      R.converge(R.equals, [
        R.pipe(
          R.prop('fn'),
          R.nth(3)
        ),
        R.pipe(
          R.prop('org'),
          R.nth(3),
          R.replace(';', '')
        ),
      ])
    )
  ),
  R.map(
    R.pipe(
      // Only pick relevant keys, if present
      R.pick(R.keys(vCardEvolveObject)),
      // Transform object to only contain strings
      R.evolve(vCardEvolveObject),
      // Add contact details key
      R.assoc('contactDetails', ''),
      // Build information for contact details from bday, phone, and email
      R.mapObjIndexed((val, key, obj) =>
        key === 'contactDetails'
          ? R.trim(
              R.replace(
                /undefined/g,
                '',
                `${R.prop('bday')(obj)} ${R.prop('tel')(obj)} ${R.prop('email')(
                  obj
                )}`
              )
            )
          : val
      ),
      // Get first and last name
      R.converge(R.assoc('lastName'), [
        R.pipe(
          R.prop('n'),
          R.head
        ),
        R.identity,
      ]),
      R.converge(R.assoc('firstName'), [
        R.pipe(
          R.prop('n'),
          R.drop(1),
          R.reduce((acc, val) => R.trim(`${acc} ${val}`), '')
        ),
        R.identity,
      ]),
      // Remove irrelevant keys
      R.dissoc('n'),
      R.dissoc('bday'),
      R.dissoc('tel'),
      R.dissoc('email'),
      // Rename relevant keys
      RA.renameKeys({
        org: 'company',
        adr: 'city',
        title: 'position',
        note: 'conversationTopics',
      })
    )
  ),
  R.merge(emptyContact)
);

Here is why I post this question on code review:

  1. The function is super long meaning it looks a little bit like spaghetti code.
  2. I'm new to functional programming and maybe I'm doing wrong things here.

If you could have a look at it and maybe improve it, make it more readable and / or performant, and teach me I would be super grateful! Thank you very much.

Fixtures:

export const vCardData = [
  {
    version: ['version', {}, 'text', '3.0'],
    prodid: ['prodid', {}, 'text', '-//Apple Inc.//iOS 10.2//EN'],
    n: ['n', {}, 'text', ['Mueller', 'Alex', '', '', '']],
    fn: ['fn', {}, 'text', 'Alex Mueller'],
    tel: ['tel', { type: ['cell', 'voice', 'pref'] }, 'text', '+555-5555-55'],
    rev: ['rev', {}, 'text', '2018-03-01T08:05:33Z'],
  },
  {
    version: ['version', {}, 'text', '3.0'],
    prodid: ['prodid', {}, 'text', '-//Apple Inc.//iOS 11.1.2//EN'],
    n: ['n', {}, 'text', ['Mueller', 'Alex', '', '', '']],
    fn: ['fn', {}, 'text', 'Alex Mueller'],
    tel: ['tel', { type: ['cell', 'voice', 'pref'] }, 'text', '+555-5555-55'],
    rev: ['rev', {}, 'text', '2018-02-14T16:01:30Z'],
  },
  {
    version: ['version', {}, 'text', '3.0'],
    prodid: ['prodid', {}, 'text', '-//Apple Inc.//Mac OS X 10.14//EN'],
    n: ['n', {}, 'text', ['', '', '', '', '']],
    fn: ['fn', {}, 'text', 'Apple GmbH'],
    org: ['org', {}, 'text', 'Apple GmbH;'],
    url: [
      'url',
      { group: 'item1', type: 'pref' },
      'text',
      'http://www.apple.com/de/',
    ],
    xAbLabel: ['xAbLabel', { group: 'item1' }, 'text', '_$!<HomePage>!$_'],
    xAbShowAs: ['xAbShowAs', {}, 'text', 'COMPANY'],
    rev: ['rev', {}, 'text', '2019-02-28T21:07:48Z'],
  },
  {
    version: ['version', {}, 'text', '3.0'],
    prodid: ['prodid', {}, 'text', '-//Apple Inc.//Mac OS X 10.14//EN'],
    n: ['n', {}, 'text', ['Howenstedt', 'Jan', '', '', '']],
    fn: ['fn', {}, 'text', 'Jan Howenstedt'],
    rev: ['rev', {}, 'text', '2019-02-28T21:07:48Z'],
  },
  {
    version: ['version', {}, 'text', '3.0'],
    prodid: ['prodid', {}, 'text', '-//Apple Inc.//iOS 12.1.4//EN'],
    n: ['n', {}, 'text', ['Vesoc', 'Mark', '', '', '']],
    fn: ['fn', {}, 'text', 'Mark Vesoc'],
    tel: [
      ['tel', { type: ['cell', 'voice', 'pref'] }, 'text', '+49 173 5555555'],
      ['tel', { type: ['work', 'voice'] }, 'text', '0241 8943850'],
    ],
    bday: ['bday', { value: 'date' }, 'date', '1991-06-26'],
    xSocialprofile: [
      'xSocialprofile',
      { type: 'whatsapp', xBundleidentifiers: 'net.whatsapp.WhatsApp' },
      'text',
      'x-a',
    ],
    pple: ['pple', {}, 'text', '%E2%80%AD+49%20173%205555555%E2%80%AC'],
    rev: ['rev', {}, 'text', '2019-04-23T11:57:59Z'],
  },
  {
    version: ['version', {}, 'text', '3.0'],
    n: ['n', {}, 'text', ['Chapoupis', 'Nik', '', '', '']],
    fn: ['fn', {}, 'text', 'Nik Chapoupis'],
    org: ['org', {}, 'text', 'Ordersome;'],
    bday: ['bday', { value: 'date' }, 'date', '1990-03-14'],
    title: ['title', {}, 'text', 'CEO'],
    note: ['note', {}, 'text', 'Looks for Designers'],
    adr: [
      [
        'adr',
        { group: 'item1', type: ['home', 'pref'] },
        'text',
        ['', '', 'Engelbertstraße 33', 'Aachen', '', '52078', 'Germany'],
      ],
      [
        'adr',
        { group: 'item2', type: 'work' },
        'text',
        ['', '', 'Jülicherstraße\\n72a', 'Aachen', '', '52070', 'Germany'],
      ],
    ],
    xAbadr: [
      ['xAbadr', { group: 'item1' }, 'text', 'de'],
      ['xAbadr', { group: 'item2' }, 'text', 'de'],
    ],
    tel: [
      ['tel', { type: ['cell', 'pref', 'voice'] }, 'text', '+49 1578 5555555'],
      ['tel', { type: ['work', 'voice'] }, 'text', '0241 8943850'],
    ],
    url: [
      'url',
      { group: 'item3', type: 'pref' },
      'text',
      'http://ordersome.de',
    ],
    xAblabel: [
      ['xAblabel', { group: 'item3' }, 'text', '_$!<HomePage>!$_'],
      ['xAblabel', { group: 'item4' }, 'text', '_$!<Anniversary>!$_'],
    ],
    impp: [
      'impp',
      { xServiceType: 'Facebook', type: ['facebook', 'pref'] },
      'text',
      'xmpp:Hello%20World',
    ],
    email: [
      [
        'email',
        { type: ['home', 'pref', 'internet'] },
        'text',
        'nikolas@gmail.com',
      ],
      [
        'email',
        { type: ['work', 'internet'] },
        'text',
        'nikolaschapoupis@gmail.com',
      ],
    ],
    xAbdate: [
      'xAbdate',
      { group: 'item4', type: 'pref' },
      'text',
      '2017-10-11',
    ],
    xSocialprofile: [
      'xSocialprofile',
      { xUser: 'twitter.com/nikolas_chap', type: 'twitter' },
      'text',
      'http://twitter',
    ],
    'com/nikolasChap': [
      'com/nikolasChap',
      { group: 'com/twitter' },
      'text',
      null,
    ],
    prodid: [
      'prodid',
      {},
      'text',
      '-//Apple Inc.//iCloud Web Address Book 1906B32//EN',
    ],
    rev: ['rev', {}, 'text', '2019-04-23T04:58:28Z'],
  },
];

export const parsedVCardData = [
  {
    city: '',
    company: '',
    contactDetails: 'Cell phone: +555-5555-55',
    conversationTopics: '',
    firstName: 'Alex',
    lastName: 'Mueller',
    position: '',
  },
  {
    city: '',
    company: '',
    contactDetails: '',
    conversationTopics: '',
    firstName: 'Jan',
    lastName: 'Howenstedt',
    position: '',
  },
  {
    city: '',
    company: '',
    contactDetails: 'Birthday: 1991-06-26 Cell phone: +49 173 5555555',
    conversationTopics: '',
    firstName: 'Mark',
    lastName: 'Vesoc',
    position: '',
  },
  {
    city: 'Aachen',
    company: 'Ordersome',
    contactDetails:
      'Birthday: 1990-03-14 Cell phone: +49 1578 5555555 Email: nikolas@gmail.com',
    conversationTopics: 'Looks for Designers',
    firstName: 'Nik',
    lastName: 'Chapoupis',
    position: 'CEO',
  },
];
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.