$row ) { $key = (string) $key; if ( '' === $key || ! is_array( $row ) ) { continue; } $status = isset( $row['status'] ) ? (string) $row['status'] : 'suggested'; $fields[ $key ] = array( 'question' => sanitize_key( isset( $row['question'] ) ? (string) $row['question'] : '' ), 'choice' => sanitize_key( isset( $row['choice'] ) ? (string) $row['choice'] : '' ), 'status' => in_array( $status, self::STATUSES, true ) ? $status : 'suggested', 'score' => isset( $row['score'] ) ? (float) $row['score'] : 0.0, ); } $policy = isset( $raw['unmapped_policy'] ) ? (string) $raw['unmapped_policy'] : 'skip'; return array( 'blueprint' => sanitize_key( isset( $raw['blueprint'] ) ? (string) $raw['blueprint'] : '' ), 'fields' => $fields, 'context' => self::clean_context( isset( $raw['context'] ) ? $raw['context'] : array() ), 'unmapped_policy' => in_array( $policy, self::POLICIES, true ) ? $policy : 'skip', ); } private static function clean_context( $raw ) { $raw = is_array( $raw ) ? $raw : array(); $out = array( 'order' => '', 'email' => '', 'customer' => '' ); foreach ( array_keys( $out ) as $role ) { if ( isset( $raw[ $role ] ) ) { $out[ $role ] = preg_replace( '/[^A-Za-z0-9_.-]/', '', (string) $raw[ $role ] ); } } return $out; } public static function save( $surface, array $map ) { $clean = array( 'blueprint' => sanitize_key( isset( $map['blueprint'] ) ? (string) $map['blueprint'] : '' ), 'fields' => array(), 'context' => self::clean_context( isset( $map['context'] ) ? $map['context'] : array() ), 'unmapped_policy' => in_array( isset( $map['unmapped_policy'] ) ? $map['unmapped_policy'] : '', self::POLICIES, true ) ? (string) $map['unmapped_policy'] : 'skip', ); foreach ( (array) ( isset( $map['fields'] ) ? $map['fields'] : array() ) as $key => $row ) { $key = (string) $key; if ( '' === $key || ! is_array( $row ) ) { continue; } $status = isset( $row['status'] ) ? (string) $row['status'] : 'suggested'; $clean['fields'][ $key ] = array( 'question' => sanitize_key( isset( $row['question'] ) ? (string) $row['question'] : '' ), 'choice' => sanitize_key( isset( $row['choice'] ) ? (string) $row['choice'] : '' ), 'status' => in_array( $status, self::STATUSES, true ) ? $status : 'suggested', 'score' => isset( $row['score'] ) ? round( (float) $row['score'], 3 ) : 0.0, ); } return update_option( self::option( $surface ), $clean, false ); } public static function question_for( $surface, $field_key ) { $map = self::get( $surface ); $key = (string) $field_key; if ( ! isset( $map['fields'][ $key ] ) ) { return ''; } $row = $map['fields'][ $key ]; return 'rejected' === $row['status'] ? '' : $row['question']; } public static function fixed_choices( $surface ) { $out = array(); foreach ( self::get( $surface )['fields'] as $key => $row ) { if ( 'rejected' !== $row['status'] && '' !== $row['choice'] ) { $out[ $key ] = $row['choice']; } } return $out; } public static function resolved( $surface ) { $out = array(); foreach ( self::get( $surface )['fields'] as $key => $row ) { if ( 'rejected' !== $row['status'] && '' !== $row['question'] ) { $out[ $key ] = $row['question']; } } return $out; } public static function infer( AIEO_Survey_Source $source, $blueprint_id ) { $surface = $source->surface(); $map = self::get( $surface ); if ( '' === $map['blueprint'] ) { $map['blueprint'] = sanitize_key( $blueprint_id ); } $blueprint_id = $map['blueprint']; $questions = array(); foreach ( AIEO_Survey_Blueprint::question_codes( $blueprint_id ) as $code ) { $q = AIEO_Survey_Blueprint::question( $code ); if ( $q ) { $questions[ $code ] = $q; } } if ( ! $questions ) { return array( 'suggested' => 0, 'kept' => 0 ); } $taken = array(); foreach ( $map['fields'] as $row ) { if ( in_array( $row['status'], self::OPERATOR_STATUSES, true ) && '' !== $row['question'] ) { $taken[ $row['question'] ] = true; } } $suggested = 0; $kept = 0; foreach ( $source->field_catalog() as $key => $field ) { $key = (string) $key; if ( isset( $map['fields'][ $key ] ) && in_array( $map['fields'][ $key ]['status'], self::OPERATOR_STATUSES, true ) ) { $kept++; continue; } $best = ''; $score = 0.0; foreach ( $questions as $code => $q ) { if ( isset( $taken[ $code ] ) ) { continue; } $s = self::score( $field, $q ); if ( $s > $score ) { $score = $s; $best = $code; } } if ( '' === $best || $score < 0.34 ) { unset( $map['fields'][ $key ] ); continue; } $map['fields'][ $key ] = array( 'question' => $best, 'choice' => '', 'status' => 'suggested', 'score' => $score ); $taken[ $best ] = true; $suggested++; } self::save( $surface, $map ); return array( 'suggested' => $suggested, 'kept' => $kept ); } public static function score( array $field, array $question ) { if ( ! self::shapes_agree( isset( $field['shape'] ) ? (string) $field['shape'] : '', $question ) ) { return 0.0; } $label = self::tokens( isset( $field['label'] ) ? $field['label'] : '' ); $admin = self::tokens( isset( $field['admin_label'] ) ? $field['admin_label'] : '' ); $code = self::tokens( $question['code'] ); $qlab = self::tokens( $question['label'] ); if ( $admin && $code && ! array_diff( $code, $admin ) ) { return 1.0; } $best = 0.0; foreach ( array( $code, $qlab ) as $target ) { $best = max( $best, self::overlap( $admin, $target ) * 0.8 ); $best = max( $best, self::jaccard( $label, $target ) ); } return $best; } private static function shapes_agree( $shape, array $question ) { if ( '' === $shape ) { return true; } if ( 'scale' === $question['scored'] ) { return in_array( $shape, array( 'scale', 'matrix' ), true ); } if ( 'yes' === $question['ordered'] ) { return 'ranking' === $shape; } if ( 'text' === $question['answers'] ) { return 'text' === $shape; } return in_array( $shape, array( 'choice', 'matrix' ), true ); } private static function tokens( $text ) { $text = (string) $text; $text = preg_replace( '/([a-z0-9])([A-Z])/', '$1 $2', $text ); $norm = AIEO_Survey_Choice_Register::normalise( $text ); if ( '' === $norm ) { return array(); } $parts = preg_split( '/[^\p{L}\p{N}]+/u', $norm, -1, PREG_SPLIT_NO_EMPTY ); $out = array(); foreach ( (array) $parts as $p ) { $len = function_exists( 'mb_strlen' ) ? mb_strlen( $p ) : strlen( $p ); if ( $len <= 2 ) { continue; } if ( $len > 3 && 's' === substr( $p, -1 ) ) { $p = substr( $p, 0, -1 ); } $out[ $p ] = true; } return array_keys( $out ); } private static function jaccard( array $a, array $b ) { if ( ! $a || ! $b ) { return 0.0; } $i = count( array_intersect( $a, $b ) ); $u = count( array_unique( array_merge( $a, $b ) ) ); return $u ? $i / $u : 0.0; } private static function overlap( array $a, array $b ) { if ( ! $a || ! $b ) { return 0.0; } return count( array_intersect( $a, $b ) ) / min( count( $a ), count( $b ) ); } public static function unmapped_questions( $surface ) { $map = self::get( $surface ); $resolved = array_flip( self::resolved( $surface ) ); $out = array(); foreach ( AIEO_Survey_Blueprint::question_codes( $map['blueprint'] ) as $code ) { if ( ! isset( $resolved[ $code ] ) ) { $out[] = $code; } } return $out; } }