$header The column names. * @param array> $rows The data rows, each a list of string cells. * @return string The CSV document, CRLF-terminated, guarded and quoted per the class contract. */ public function write( array $header, array $rows ): string { $out = $this->record( $header ); foreach ( $rows as $row ) { $out .= $this->record( $row ); } return $out; } /** * Guard, quote and serialize one record. * * @since 1.0.0 * * @param array $fields The record's cells. * @return string The CRLF-terminated record. */ private function record( array $fields ): string { $cells = array(); foreach ( $fields as $field ) { $cells[] = $this->quote( $this->guard( $field ) ); } return implode( ',', $cells ) . self::CRLF; } /** * Prefix formula-shaped text with a quote. * * Decimal numbers remain numeric for spreadsheet totals. * * @since 1.0.0 * * @param string $field The raw cell value. * @return string The guarded value. */ private function guard( string $field ): string { if ( '' === $field || 1 === preg_match( self::LOOKS_NUMERIC, $field ) ) { return $field; } if ( false !== strpos( self::FORMULA_LEADS, $field[0] ) ) { return "'" . $field; } return $field; } /** * Quote a field only when RFC 4180 requires it. * * @since 1.0.0 * * @param string $field The (already guarded) cell value. * @return string The field, enclosed and escaped only if it needs it. */ private function quote( string $field ): string { if ( 1 !== preg_match( '/[",\r\n]/', $field ) ) { return $field; } return '"' . str_replace( '"', '""', $field ) . '"'; } }